Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.

/**
 * Web CT question importer.
 *
 * @package    qformat_webct
 * @copyright  2004 ASP Consulting http://www.asp-consulting.net
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */


defined('MOODLE_INTERNAL') || die();

/**
 * Manipulate HTML editites in a string. Used by WebCT import.
 * @param string $string
 * @return string
 */
function unhtmlentities($string) {
    $search = array ("'<script[?>]*?>.*?</script>'si",  // Remove javascript.
                 "'<[\/\!]*?[^<?>]*?>'si",  // Remove HTML tags.
                 "'([\r\n])[\s]+'",  // Remove spaces.
                 "'&(quot|#34);'i",  // Remove HTML entites.
                 "'&(amp|#38);'i",
                 "'&(lt|#60);'i",
                 "'&(gt|#62);'i",
                 "'&(nbsp|#160);'i",
                 "'&(iexcl|#161);'i",
                 "'&(cent|#162);'i",
                 "'&(pound|#163);'i",
                 "'&(copy|#169);'i",
                 "'&#(\d+);'e");  // Evaluate like PHP.
    $replace = array ("",
                  "",
                  "\\1",
                  "\"",
                  "&",
                  "<",
                  "?>",
                  " ",
                  chr(161),
                  chr(162),
                  chr(163),
                  chr(169),
                  "chr(\\1)");
    return preg_replace ($search, $replace, $string);
}

/**
 * Helper function for WebCT import.
 * @param unknown_type $formula
 */
function qformat_webct_convert_formula($formula) {

    // Remove empty space, as it would cause problems otherwise.
    $formula = str_replace(' ', '', $formula);

    // Remove paranthesis after e,E and *10**.
    while (preg_match('~[0-9.](e|E|\\*10\\*\\*)\\([+-]?[0-9]+\\)~', $formula, $regs)) {
        $formula = str_replace(
                $regs[0], preg_replace('/[)(]/', '', $regs[0]), $formula);
    }

    // Replace *10** with e where possible.
    while (preg_match('~(^[+-]?|[^eE][+-]|[^0-9eE+-])[0-9.]+\\*10\\*\\*[+-]?[0-9]+([^0-9.eE]|$)~',
            $formula, $regs)) {
        $formula = str_replace(
                $regs[0], str_replace('*10**', 'e', $regs[0]), $formula);
    }

    // Replace other 10** with 1e where possible.
    while (preg_match('~(^|[^0-9.eE])10\\*\\*[+-]?[0-9]+([^0-9.eE]|$)~', $formula, $regs)) {
        $formula = str_replace(
                $regs[0], str_replace('10**', '1e', $regs[0]), $formula);
    }

    // Replace all other base**exp with the PHP equivalent function pow(base,exp)
    // (Pretty tricky to exchange an operator with a function).
    while (2 == count($splits = explode('**', $formula, 2))) {

        // Find $base.
        if (preg_match('~^(.*[^0-9.eE])?(([0-9]+(\\.[0-9]*)?|\\.[0-9]+)([eE][+-]?[0-9]+)?|\\{[^}]*\\})$~',
                $splits[0], $regs)) {
            // The simple cases.
            $base = $regs[2];
            $splits[0] = $regs[1];

        } else if (preg_match('~\\)$~', $splits[0])) {
            // Find the start of this parenthesis.
            $deep = 1;
            for ($i = 1; $deep; ++$i) {
                if (!preg_match('~^(.*[^[:alnum:]_])?([[:alnum:]_]*([)(])([^)(]*[)(]){'.$i.'})$~',
                        $splits[0], $regs)) {
                    print_error('parenthesisinproperstart', 'question', '', $splits[0]);
                }
                if ('(' == $regs[3]) {
                    --$deep;
                } else if (')' == $regs[3]) {
                    ++$deep;
                } else {
                    print_error('impossiblechar', 'question', '', $regs[3]);
                }
            }
            $base = $regs[2];
            $splits[0] = $regs[1];

        } else {
            print_error('badbase', 'question', '', $splits[0]);
        }

        // Find $exp (similar to above but a little easier).
        if (preg_match('~^([+-]?(\\{[^}]\\}|([0-9]+(\\.[0-9]*)?|\\.[0-9]+)([eE][+-]?[0-9]+)?))(.*)~',
                $splits[1], $regs)) {
            // The simple case.
            $exp = $regs[1];
            $splits[1] = $regs[6];

        } else if (preg_match('~^[+-]?[[:alnum:]_]*\\(~', $splits[1])) {
            // Find the end of the parenthesis.
            $deep = 1;
            for ($i = 1; $deep; ++$i) {
                if (!preg_match('~^([+-]?[[:alnum:]_]*([)(][^)(]*){'.$i.'}([)(]))(.*)~',
                        $splits[1], $regs)) {
                    print_error('parenthesisinproperclose', 'question', '', $splits[1]);
                }
                if (')' == $regs[3]) {
                    --$deep;
                } else if ('(' == $regs[3]) {
                    ++$deep;
                } else {
                    print_error('impossiblechar', 'question');
                }
            }
            $exp = $regs[1];
            $splits[1] = $regs[4];
        }

        // Replace it!
        $formula = "{$splits[0]}pow({$base},{$exp}){$splits[1]}";
    }

    // Nothing more is known to need to be converted.

    return $formula;
}


/**
 * Web CT question importer.
 *
 * @copyright  2004 ASP Consulting http://www.asp-consulting.net
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class qformat_webct extends qformat_default {
    /** @var string path to the temporary directory. */
    public $tempdir = '';

    /**
     * This plugin provide import
     * @return bool true
     */
    public function provide_import() {
        return true;
    }

    public function can_import_file($file) {
        $mimetypes = array(
            mimeinfo('type', '.txt'),
            mimeinfo('type', '.zip')
        );
        return in_array($file->get_mimetype(), $mimetypes);
    }

    public function mime_type() {
        return mimeinfo('type', '.zip');
    }

    /**
> * Validate the given file. * Store an image file in a draft filearea > * * @param array $text, if itemid element don't exists it will be created > * For more expensive or detailed integrity checks. * @param string tempdir path to root of image tree > * * @param string filepathinsidetempdir path to image in the tree > * @param stored_file $file the file to check * @param string filename image's name > * @return string the error message that occurred while validating the given file * @return string new name of the image as it was stored > */ */ > public function validate_file(stored_file $file): string { protected function store_file_for_text_field(&$text, $tempdir, $filepathinsidetempdir, $filename) { > return $this->validate_is_utf8_file($file); global $USER; > } $fs = get_file_storage(); > if (empty($text['itemid'])) { > /**
$text['itemid'] = file_get_unused_draft_itemid(); } // As question file areas don't support subdirs, // convert path to filename. // So that images with same name can be imported. $newfilename = clean_param(str_replace('/', '__', $filepathinsidetempdir . '__' . $filename), PARAM_FILE); $filerecord = array( 'contextid' => context_user::instance($USER->id)->id, 'component' => 'user', 'filearea' => 'draft', 'itemid' => $text['itemid'], 'filepath' => '/', 'filename' => $newfilename, ); $fs->create_file_from_pathname($filerecord, $tempdir . '/' . $filepathinsidetempdir . '/' . $filename); return $newfilename; } /** * Given an HTML text with references to images files, * store all images in a draft filearea, * and return an array with all urls in text recoded, * format set to FORMAT_HTML, and itemid set to filearea itemid * @param string text text to parse and recode * @return array with keys text, format, itemid. */ public function text_field($text) { $data = array(); // Step one, find all file refs then add to array. preg_match_all('|<img[^>]+src="([^"]*)"|i', $text, $out); // Find all src refs. $filepaths = array(); foreach ($out[1] as $path) { $fullpath = $this->tempdir . '/' . $path; if (is_readable($fullpath) && !in_array($path, $filepaths)) { $dirpath = dirname($path); $filename = basename($path); $newfilename = $this->store_file_for_text_field($data, $this->tempdir, $dirpath, $filename); $text = preg_replace("|{$path}|", "@@PLUGINFILE@@/" . $newfilename, $text); $filepaths[] = $path; } } $data['text'] = $text; $data['format'] = FORMAT_HTML; return $data; } /** * Does any post-processing that may be desired * Clean the temporary directory if a zip file was imported * @return bool success */ public function importpostprocess() { if (!empty($this->tempdir)) { fulldelete($this->tempdir); } return true; } /** * Return content of all files containing questions, * as an array one element for each file found, * For each file, the corresponding element is an array of lines. * @param string filename name of file * @return mixed contents array or false on failure */ public function readdata($filename) { // Find if we are importing a .txt file. if (strtolower(pathinfo($filename, PATHINFO_EXTENSION)) == 'txt') { if (!is_readable($filename)) { $this->error(get_string('filenotreadable', 'error')); return false; } return file($filename); } // We are importing a zip file. // Create name for temporary directory. $this->tempdir = make_request_directory(); if (is_readable($filename)) { if (!copy($filename, $this->tempdir . '/webct.zip')) { $this->error(get_string('cannotcopybackup', 'question')); fulldelete($this->tempdir); return false; } $packer = get_file_packer('application/zip'); if ($packer->extract_to_pathname($this->tempdir . '/webct.zip', $this->tempdir, null, null, true)) { $dir = $this->tempdir; if ((($handle = opendir($dir))) == false) { // The directory could not be opened. fulldelete($this->tempdir); return false; } // Create arrays to store files and directories. $dirfiles = array(); $dirsubdirs = array(); $slash = '/'; // Loop through all directory entries, and construct two temporary arrays containing files and sub directories. while (false !== ($entry = readdir($handle))) { if (is_dir($dir. $slash .$entry) && $entry != '..' && $entry != '.') { $dirsubdirs[] = $dir. $slash .$entry; } else if ($entry != '..' && $entry != '.') { $dirfiles[] = $dir. $slash .$entry; } } if ((($handle = opendir($dirsubdirs[0]))) == false) { // The directory could not be opened. fulldelete($this->tempdir); return false; } while (false !== ($entry = readdir($handle))) { if (is_dir($dirsubdirs[0]. $slash .$entry) && $entry != '..' && $entry != '.') { $dirsubdirs[] = $dirsubdirs[0]. $slash .$entry; } else if ($entry != '..' && $entry != '.') { $dirfiles[] = $dirsubdirs[0]. $slash .$entry; } } return file($dirfiles[1]); } else { $this->error(get_string('cannotunzip', 'question')); fulldelete($this->tempdir); } } else { $this->error(get_string('cannotreaduploadfile', 'error')); fulldelete($this->tempdir); } return false; } public function readquestions ($lines) { $webctnumberregex = '[+-]?([0-9]+(\\.[0-9]*)?|\\.[0-9]+)((e|E|\\*10\\*\\*)([+-]?[0-9]+|\\([+-]?[0-9]+\\)))?'; $questions = array(); $warnings = array(); $webctoptions = array(); $ignorerestofquestion = false; $nlinecounter = 0; $nquestionstartline = 0; $bishtmltext = false; $lines[] = ":EOF:"; // For an easiest processing of the last line. // We don't call defaultquestion() here, it will be called later. foreach ($lines as $line) { $nlinecounter++; $line = core_text::convert($line, 'windows-1252', 'utf-8'); // Processing multiples lines strings. if (isset($questiontext) and is_string($questiontext)) { if (preg_match("~^:~", $line)) { $questiontext = $this->text_field(trim($questiontext)); $question->questiontext = $questiontext['text']; $question->questiontextformat = $questiontext['format']; if (isset($questiontext['itemid'])) { $question->questiontextitemid = $questiontext['itemid']; } unset($questiontext); } else { $questiontext .= str_replace('\:', ':', $line); continue; } } if (isset($answertext) and is_string($answertext)) { if (preg_match("~^:~", $line)) { $answertext = trim($answertext); if ($question->qtype == 'multichoice' || $question->qtype == 'match' ) { $question->answer[$currentchoice] = $this->text_field($answertext); $question->subanswers[$currentchoice] = $question->answer[$currentchoice]; } else { $question->answer[$currentchoice] = $answertext; $question->subanswers[$currentchoice] = $answertext; } unset($answertext); } else { $answertext .= str_replace('\:', ':', $line); continue; } } if (isset($responsetext) and is_string($responsetext)) { if (preg_match("~^:~", $line)) { $question->subquestions[$currentchoice] = trim($responsetext); unset($responsetext); } else { $responsetext .= str_replace('\:', ':', $line); continue; } } if (isset($feedbacktext) and is_string($feedbacktext)) { if (preg_match("~^:~", $line)) { $question->feedback[$currentchoice] = $this->text_field(trim($feedbacktext)); unset($feedbacktext); } else { $feedbacktext .= str_replace('\:', ':', $line); continue; } } if (isset($generalfeedbacktext) and is_string($generalfeedbacktext)) { if (preg_match("~^:~", $line)) { $question->tempgeneralfeedback = trim($generalfeedbacktext); unset($generalfeedbacktext); } else { $generalfeedbacktext .= str_replace('\:', ':', $line); continue; } } if (isset($graderinfo) and is_string($graderinfo)) { if (preg_match("~^:~", $line)) { $question->graderinfo['text'] = trim($graderinfo); $question->graderinfo['format'] = FORMAT_HTML; unset($graderinfo); } else { $graderinfo .= str_replace('\:', ':', $line); continue; } } $line = trim($line); if (preg_match("~^:(TYPE|EOF):~i", $line)) { // New Question or End of File. if (isset($question)) { // If previous question exists, complete, check and save it. // Setup default value of missing fields. if (!isset($question->name)) { $question->name = $this->create_default_question_name( $question->questiontext, get_string('questionname', 'question')); } if (!isset($question->defaultmark)) { $question->defaultmark = 1; } if (!isset($question->image)) { $question->image = ''; } // Perform sanity checks. $questionok = true; if (strlen($question->questiontext) == 0) { $warnings[] = get_string('missingquestion', 'qformat_webct', $nquestionstartline); $questionok = false; } if (count($question->answer) < 1) { // A question must have at least 1 answer. $this->error(get_string('missinganswer', 'qformat_webct', $nquestionstartline), '', $question->name); $questionok = false; } else { // Create empty feedback array. foreach ($question->answer as $key => $dataanswer) { if (!isset($question->feedback[$key])) { $question->feedback[$key]['text'] = ''; $question->feedback[$key]['format'] = FORMAT_HTML; } } // This tempgeneralfeedback allows the code to work with versions from 1.6 to 1.9. // When question->generalfeedback is undefined, the webct feedback is added to each answer feedback. if (isset($question->tempgeneralfeedback)) { if (isset($question->generalfeedback)) { $generalfeedback = $this->text_field($question->tempgeneralfeedback); $question->generalfeedback = $generalfeedback['text']; $question->generalfeedbackformat = $generalfeedback['format']; if (isset($generalfeedback['itemid'])) { $question->genralfeedbackitemid = $generalfeedback['itemid']; } } else { foreach ($question->answer as $key => $dataanswer) { if ($question->tempgeneralfeedback != '') { $question->feedback[$key]['text'] = $question->tempgeneralfeedback .'<br/>'.$question->feedback[$key]['text']; } } } unset($question->tempgeneralfeedback); } $maxfraction = -1; $totalfraction = 0; foreach ($question->fraction as $fraction) { if ($fraction > 0) { $totalfraction += $fraction; } if ($fraction > $maxfraction) { $maxfraction = $fraction; } } switch ($question->qtype) { case 'shortanswer': if ($maxfraction != 1) { $maxfraction = $maxfraction * 100; $this->error(get_string('wronggrade', 'qformat_webct', $nlinecounter) .' '.get_string('fractionsnomax', 'question', $maxfraction), '', $question->name);; $questionok = false; } break; case 'multichoice': $question = $this->add_blank_combined_feedback($question); if ($question->single) { if ($maxfraction != 1) { $maxfraction = $maxfraction * 100; $this->error(get_string('wronggrade', 'qformat_webct', $nlinecounter) .' '.get_string('fractionsnomax', 'question', $maxfraction), '', $question->name); $questionok = false; } } else { $totalfraction = round($totalfraction, 2); if ($totalfraction != 1) { $totalfraction = $totalfraction * 100; $this->error(get_string('wronggrade', 'qformat_webct', $nlinecounter) .' '.get_string('fractionsaddwrong', 'qtype_multichoice', $totalfraction), '', $question->name); $questionok = false; } } break; case 'calculated': foreach ($question->answer as $answer) { if ($formulaerror = qtype_calculated_find_formula_errors($answer)) { $warnings[] = "'{$question->name}': ". $formulaerror; $questionok = false; } } foreach ($question->dataset as $dataset) { $dataset->itemcount = count($dataset->datasetitem); } $question->import_process = true; break; case 'match': // MDL-10680: // Switch subquestions and subanswers. $question = $this->add_blank_combined_feedback($question); foreach ($question->subquestions as $id => $subquestion) { $temp = $question->subquestions[$id]; $question->subquestions[$id] = $question->subanswers[$id]; $question->subanswers[$id] = $temp; } if (count($question->answer) < 3) { // Add a dummy missing question. $question->name = 'Dummy question added '.$question->name; $question->answer[] = 'dummy'; $question->subanswers[] = 'dummy'; $question->subquestions[] = 'dummy'; $question->fraction[] = '0.0'; $question->feedback[] = ''; } break; default: // No problemo. } } if ($questionok) { $questions[] = $question; // Store it. unset($question); // And prepare a new one. $question = $this->defaultquestion(); } } $nquestionstartline = $nlinecounter; } // Processing Question Header. if (preg_match("~^:TYPE:MC:1(.*)~i", $line, $webctoptions)) { // Multiple Choice Question with only one good answer. $question = $this->defaultquestion(); $question->feedback = array(); $question->qtype = 'multichoice'; $question->single = 1; // Only one answer is allowed. $ignorerestofquestion = false; continue; } if (preg_match("~^:TYPE:MC:N(.*)~i", $line, $webctoptions)) { // Multiple Choice Question with several good answers. $question = $this->defaultquestion(); $question->feedback = array(); $question->qtype = 'multichoice'; $question->single = 0; // Many answers allowed. $ignorerestofquestion = false; continue; } if (preg_match("~^:TYPE:S~i", $line)) { // Short Answer Question. $question = $this->defaultquestion(); $question->feedback = array(); $question->qtype = 'shortanswer'; $question->usecase = 0; // Ignore case. $ignorerestofquestion = false; continue; } if (preg_match("~^:TYPE:C~i", $line)) { // Calculated Question. $question = $this->defaultquestion(); $question->qtype = 'calculated'; $question->answer = array(); // No problem as they go as :FORMULA: from webct. $question->units = array(); $question->dataset = array(); $question->fraction = array('1.0'); $question->feedback = array(); $currentchoice = -1; $ignorerestofquestion = false; continue; } if (preg_match("~^:TYPE:M~i", $line)) { // Match Question. $question = $this->defaultquestion(); $question->qtype = 'match'; $question->feedback = array(); $ignorerestofquestion = false; // Match question processing is not debugged. continue; } if (preg_match("~^:TYPE:P~i", $line)) { // Paragraph Question. $question = $this->defaultquestion(); $question->qtype = 'essay'; $question->responseformat = 'editor'; $question->responserequired = 1; $question->responsefieldlines = 15; $question->attachments = 0; $question->attachmentsrequired = 0; $question->graderinfo = array( 'text' => '', 'format' => FORMAT_HTML, ); $question->feedback = array(); $question->generalfeedback = ''; $question->generalfeedbackformat = FORMAT_HTML; $question->generalfeedbackfiles = array(); $question->responsetemplate = $this->text_field(''); $question->questiontextformat = FORMAT_HTML; $ignorerestofquestion = false; // To make us pass the end-of-question sanity checks. $question->answer = array('dummy'); $question->fraction = array('1.0'); continue; } if (preg_match("~^:TYPE:~i", $line)) { // Unknow question type. $warnings[] = get_string('unknowntype', 'qformat_webct', $nlinecounter); unset($question); $ignorerestofquestion = true; // Question Type not handled by Moodle. continue; } if ($ignorerestofquestion) { continue; } if (preg_match("~^:TITLE:(.*)~i", $line, $webctoptions)) { $name = trim($webctoptions[1]); $question->name = $this->clean_question_name($name); continue; } if (preg_match("~^:IMAGE:(.*)~i", $line, $webctoptions)) { $filename = trim($webctoptions[1]); if (preg_match("~^http://~i", $filename)) { $question->image = $filename; } continue; } // Need to put the parsing of calculated items here to avoid ambitiuosness: // if question isn't defined yet there is nothing to do here (avoid notices). if (!isset($question)) { continue; } if (isset($question->qtype ) && 'calculated' == $question->qtype && preg_match( "~^:([[:lower:]].*|::.*)-(MIN|MAX|DEC|VAL([0-9]+))::?:?({$webctnumberregex})~", $line, $webctoptions)) { $datasetname = preg_replace('/^::/', '', $webctoptions[1]); $datasetvalue = qformat_webct_convert_formula($webctoptions[4]); switch ($webctoptions[2]) { case 'MIN': $question->dataset[$datasetname]->min = $datasetvalue; break; case 'MAX': $question->dataset[$datasetname]->max = $datasetvalue; break; case 'DEC': $datasetvalue = floor($datasetvalue); // Int only! $question->dataset[$datasetname]->length = max(0, $datasetvalue); break; default: // The VAL case. $question->dataset[$datasetname]->datasetitem[$webctoptions[3]] = new stdClass(); $question->dataset[$datasetname]->datasetitem[$webctoptions[3]]->itemnumber = $webctoptions[3]; $question->dataset[$datasetname]->datasetitem[$webctoptions[3]]->value = $datasetvalue; break; } continue; } $bishtmltext = preg_match("~:H$~i", $line); // True if next lines are coded in HTML. if (preg_match("~^:QUESTION~i", $line)) { $questiontext = ''; // Start gathering next lines. continue; } if (preg_match("~^:ANSWER([0-9]+):([^:]+):([0-9\.\-]+):(.*)~i", $line, $webctoptions)) { // Shortanswer. $currentchoice = $webctoptions[1]; $answertext = $webctoptions[2]; // Start gathering next lines. $question->fraction[$currentchoice] = ($webctoptions[3]/100); continue; } if (preg_match("~^:ANSWER([0-9]+):([0-9\.\-]+)~i", $line, $webctoptions)) { $answertext = ''; // Start gathering next lines. $currentchoice = $webctoptions[1]; $question->fraction[$currentchoice] = ($webctoptions[2]/100); continue; } if (preg_match('~^:ANSWER:~i', $line)) { // Essay. $graderinfo = ''; // Start gathering next lines. continue; } if (preg_match('~^:FORMULA:(.*)~i', $line, $webctoptions)) { // Answer for a calculated question. ++$currentchoice; $question->answer[$currentchoice] = qformat_webct_convert_formula($webctoptions[1]); // Default settings. $question->fraction[$currentchoice] = 1.0; $question->tolerance[$currentchoice] = 0.0; $question->tolerancetype[$currentchoice] = 2; // Nominal (units in webct). $question->feedback[$currentchoice]['text'] = ''; $question->feedback[$currentchoice]['format'] = FORMAT_HTML; $question->correctanswerlength[$currentchoice] = 4; $datasetnames = question_bank::get_qtype('calculated')->find_dataset_names($webctoptions[1]); foreach ($datasetnames as $datasetname) { $question->dataset[$datasetname] = new stdClass(); $question->dataset[$datasetname]->datasetitem = array(); $question->dataset[$datasetname]->name = $datasetname; $question->dataset[$datasetname]->distribution = 'uniform'; $question->dataset[$datasetname]->status = 'private'; } continue; } if (preg_match("~^:L([0-9]+)~i", $line, $webctoptions)) { $answertext = ''; // Start gathering next lines. $currentchoice = $webctoptions[1]; $question->fraction[$currentchoice] = 1; continue; } if (preg_match("~^:R([0-9]+)~i", $line, $webctoptions)) { $responsetext = ''; // Start gathering next lines. $currentchoice = $webctoptions[1]; continue; } if (preg_match("~^:REASON([0-9]+):?~i", $line, $webctoptions)) { $feedbacktext = ''; // Start gathering next lines. $currentchoice = $webctoptions[1]; continue; } if (preg_match("~^:FEEDBACK([0-9]+):?~i", $line, $webctoptions)) { $generalfeedbacktext = ''; // Start gathering next lines. $currentchoice = $webctoptions[1]; continue; } if (preg_match('~^:FEEDBACK:(.*)~i', $line, $webctoptions)) { $generalfeedbacktext = ''; // Start gathering next lines. continue; } if (preg_match('~^:LAYOUT:(.*)~i', $line, $webctoptions)) { // Ignore since layout in question_multichoice is no more used in Moodle. // $webctoptions[1] contains either vertical or horizontal. continue; } if (isset($question->qtype ) && 'calculated' == $question->qtype && preg_match('~^:ANS-DEC:([1-9][0-9]*)~i', $line, $webctoptions)) { // We can but hope that this always appear before the ANSTYPE property. $question->correctanswerlength[$currentchoice] = $webctoptions[1]; continue; } if (isset($question->qtype )&& 'calculated' == $question->qtype && preg_match("~^:TOL:({$webctnumberregex})~i", $line, $webctoptions)) { // We can but hope that this always appear before the TOL property. $question->tolerance[$currentchoice] = qformat_webct_convert_formula($webctoptions[1]); continue; } if (isset($question->qtype )&& 'calculated' == $question->qtype && preg_match('~^:TOLTYPE:percent~i', $line)) { // Percentage case is handled as relative in Moodle. $question->tolerance[$currentchoice] /= 100; $question->tolerancetype[$currentchoice] = 1; // Relative. continue; } if (preg_match('~^:UNITS:(.+)~i', $line, $webctoptions) and $webctunits = trim($webctoptions[1])) { // This is a guess - I really do not know how different webct units are separated... $webctunits = explode(':', $webctunits); $unitrec->multiplier = 1.0; // Webct does not seem to support this. foreach ($webctunits as $webctunit) { $unitrec->unit = trim($webctunit); $question->units[] = $unitrec; } continue; } if (!empty($question->units) && preg_match('~^:UNITREQ:(.*)~i', $line, $webctoptions) && !$webctoptions[1]) { // There are units but units are not required so add the no unit alternative. // We can but hope that the UNITS property always appear before this property. $unitrec->unit = ''; $unitrec->multiplier = 1.0; $question->units[] = $unitrec; continue; } if (!empty($question->units) && preg_match('~^:UNITCASE:~i', $line)) { // This could be important but I was not able to figure out how // it works so I ignore it for now. continue; } if (isset($question->qtype )&& 'calculated' == $question->qtype && preg_match('~^:ANSTYPE:dec~i', $line)) { $question->correctanswerformat[$currentchoice] = '1'; continue; } if (isset($question->qtype )&& 'calculated' == $question->qtype && preg_match('~^:ANSTYPE:sig~i', $line)) { $question->correctanswerformat[$currentchoice] = '2'; continue; } } if (count($warnings) > 0) { echo '<p>'.get_string('warningsdetected', 'qformat_webct', count($warnings)).'</p><ul>'; foreach ($warnings as $warning) { echo "<li>{$warning}</li>"; } echo '</ul>'; } return $questions; } }