"MDL-12304, fix double text"
[moodle-linuxchix.git] / question / format / hotpot / format.php
blobde91d43fa635b25897380bef97670f47687b5b2f
1 <?PHP // $Id$
2 ////////////////////////////////////////////////////////////////////////////
3 /// Hotpotatoes 5.0 and 6.0 Format
4 ///
5 /// This Moodle class provides all functions necessary to import
6 /// (export is not implemented ... yet)
7 ///
8 ////////////////////////////////////////////////////////////////////////////
10 // Based on default.php, included by ../import.php
11 /**
12 * @package questionbank
13 * @subpackage importexport
16 class qformat_hotpot extends qformat_default {
18 function provide_import() {
19 return true;
22 function readquestions ($lines) {
23 /// Parses an array of lines into an array of questions,
24 /// where each item is a question object as defined by
25 /// readquestion().
27 // set courseid and baseurl
28 global $CFG, $COURSE, $course;
29 switch (true) {
30 case isset($this->course->id):
31 // import to quiz module
32 $courseid = $this->course->id;
33 break;
34 case isset($course->id):
35 // import to lesson module
36 $courseid = $course->id;
37 break;
38 case isset($COURSE->id):
39 // last resort
40 $courseid = $COURSE->id;
41 break;
42 default:
43 // shouldn't happen !!
44 $courseid = 0;
46 if ($CFG->slasharguments) {
47 $baseurl = "$CFG->wwwroot/file.php/$courseid/";
48 } else {
49 $baseurl = "$CFG->wwwroot/file.php?file=/$courseid/";
52 // get import file name
53 global $params;
54 if (isset($params) && !empty($params->choosefile)) {
55 // course file (Moodle >=1.6+)
56 $filename = $params->choosefile;
57 } else {
58 // uploaded file (all Moodles)
59 $filename = basename($_FILES['newfile']['tmp_name']);
62 // get hotpot file source
63 $source = implode($lines, " ");
64 $source = hotpot_convert_relative_urls($source, $baseurl, $filename);
66 // create xml tree for this hotpot
67 $xml = new hotpot_xml_tree($source);
69 // determine the quiz type
70 $xml->quiztype = '';
71 $keys = array_keys($xml->xml);
72 foreach ($keys as $key) {
73 if (preg_match('/^(hotpot|textoys)-(\w+)-file$/i', $key, $matches)) {
74 $xml->quiztype = strtolower($matches[2]);
75 $xml->xml_root = "['$key']['#']";
76 break;
80 // convert xml to questions array
81 $questions = array();
82 switch ($xml->quiztype) {
83 case 'jcloze':
84 $this->process_jcloze($xml, $questions);
85 break;
86 case 'jcross':
87 $this->process_jcross($xml, $questions);
88 break;
89 case 'jmatch':
90 $this->process_jmatch($xml, $questions);
91 break;
92 case 'jmix':
93 $this->process_jmix($xml, $questions);
94 break;
95 case 'jbc':
96 case 'jquiz':
97 $this->process_jquiz($xml, $questions);
98 break;
99 default:
100 if (empty($xml->quiztype)) {
101 notice("Input file not recognized as a Hot Potatoes XML file");
102 } else {
103 notice("Unknown quiz type '$xml->quiztype'");
105 } // end switch
106 return $questions;
109 function process_jcloze(&$xml, &$questions) {
110 // define default grade (per cloze gap)
111 $defaultgrade = 1;
112 $gap_count = 0;
114 // detect old Moodles (1.4 and earlier)
115 global $CFG, $db;
116 $moodle_14 = false;
117 if ($columns = $db->MetaColumns("{$CFG->prefix}question_multianswer")) {
118 foreach ($columns as $column) {
119 if ($column->name=='answers' || $column->name=='positionkey' || $column->name=='answertype' || $column->name=='norm') {
120 $moodle_14 = true;
125 // xml tags for the start of the gap-fill exercise
126 $tags = 'data,gap-fill';
128 $x = 0;
129 while (($exercise = "[$x]['#']") && $xml->xml_value($tags, $exercise)) {
130 // there is usually only one exercise in a file
132 if (method_exists($this, 'defaultquestion')) {
133 $question = $this->defaultquestion();
134 } else {
135 $question = new stdClass();
136 $question->usecase = 0; // Ignore case
137 $question->image = ""; // No images with this format
139 $question->qtype = MULTIANSWER;
141 $question->name = $this->hotpot_get_title($xml, $x);
142 $question->questiontext = $this->hotpot_get_reading($xml);
144 // setup answer arrays
145 if ($moodle_14) {
146 $question->answers = array();
147 } else {
148 global $COURSE; // initialized in questions/import.php
149 $question->course = $COURSE->id;
150 $question->options = new stdClass();
151 $question->options->questions = array(); // one for each gap
154 $q = 0;
155 while ($text = $xml->xml_value($tags, $exercise."[$q]")) {
156 // add next bit of text
157 $question->questiontext .= $this->hotpot_prepare_str($text);
159 // check for a gap
160 $question_record = $exercise."['question-record'][$q]['#']";
161 if ($xml->xml_value($tags, $question_record)) {
163 // add gap
164 $gap_count ++;
165 $positionkey = $q+1;
166 $question->questiontext .= '{#'.$positionkey.'}';
168 // initialize answer settings
169 if ($moodle_14) {
170 $question->answers[$q]->positionkey = $positionkey;
171 $question->answers[$q]->answertype = SHORTANSWER;
172 $question->answers[$q]->norm = $defaultgrade;
173 $question->answers[$q]->alternatives = array();
174 } else {
175 $wrapped = new stdClass();
176 $wrapped->qtype = SHORTANSWER;
177 $wrapped->usecase = 0;
178 $wrapped->defaultgrade = $defaultgrade;
179 $wrapped->questiontextformat = 0;
180 $wrapped->answer = array();
181 $wrapped->fraction = array();
182 $wrapped->feedback = array();
183 $answers = array();
186 // add answers
187 $a = 0;
188 while (($answer=$question_record."['answer'][$a]['#']") && $xml->xml_value($tags, $answer)) {
189 $text = $this->hotpot_prepare_str($xml->xml_value($tags, $answer."['text'][0]['#']"));
190 $correct = $xml->xml_value($tags, $answer."['correct'][0]['#']");
191 $feedback = $this->hotpot_prepare_str($xml->xml_value($tags, $answer."['feedback'][0]['#']"));
192 if ($text) {
193 // set score (0=0%, 1=100%)
194 $fraction = empty($correct) ? 0 : 1;
195 // store answer
196 if ($moodle_14) {
197 $question->answers[$q]->alternatives[$a] = new stdClass();
198 $question->answers[$q]->alternatives[$a]->answer = $text;
199 $question->answers[$q]->alternatives[$a]->fraction = $fraction;
200 $question->answers[$q]->alternatives[$a]->feedback = $feedback;
201 } else {
202 $wrapped->answer[] = $text;
203 $wrapped->fraction[] = $fraction;
204 $wrapped->feedback[] = $feedback;
205 $answers[] = (empty($fraction) ? '' : '=').$text.(empty($feedback) ? '' : ('#'.$feedback));
208 $a++;
210 // compile answers into question text, if necessary
211 if ($moodle_14) {
212 // do nothing
213 } else {
214 $wrapped->questiontext = '{'.$defaultgrade.':SHORTANSWER:'.implode('~', $answers).'}';
215 $question->options->questions[] = $wrapped;
217 } // end if gap
218 $q++;
219 } // end while $text
221 // define total grade for this exercise
222 $question->defaultgrade = $gap_count * $defaultgrade;
224 $questions[] = $question;
225 $x++;
226 } // end while $exercise
229 function process_jcross(&$xml, &$questions) {
230 // xml tags to the start of the crossword exercise clue items
231 $tags = 'data,crossword,clues,item';
233 $x = 0;
234 while (($item = "[$x]['#']") && $xml->xml_value($tags, $item)) {
236 $text = $xml->xml_value($tags, $item."['def'][0]['#']");
237 $answer = $xml->xml_value($tags, $item."['word'][0]['#']");
239 if ($text && $answer) {
240 if (method_exists($this, 'defaultquestion')) {
241 $question = $this->defaultquestion();
242 } else {
243 $question = new stdClass();
244 $question->usecase = 0; // Ignore case
245 $question->image = ""; // No images with this format
247 $question->qtype = SHORTANSWER;
248 $question->name = $this->hotpot_get_title($xml, $x, true);
250 $question->questiontext = $this->hotpot_prepare_str($text);
251 $question->answer = array($this->hotpot_prepare_str($answer));
252 $question->fraction = array(1);
253 $question->feedback = array('');
255 $questions[] = $question;
257 $x++;
261 function process_jmatch(&$xml, &$questions) {
262 // define default grade (per matched pair)
263 $defaultgrade = 1;
264 $match_count = 0;
266 // xml tags to the start of the matching exercise
267 $tags = 'data,matching-exercise';
269 $x = 0;
270 while (($exercise = "[$x]['#']") && $xml->xml_value($tags, $exercise)) {
271 // there is usually only one exercise in a file
273 if (method_exists($this, 'defaultquestion')) {
274 $question = $this->defaultquestion();
275 } else {
276 $question = new stdClass();
277 $question->usecase = 0; // Ignore case
278 $question->image = ""; // No images with this format
280 $question->qtype = MATCH;
281 $question->name = $this->hotpot_get_title($xml, $x);
283 $question->questiontext = $this->hotpot_get_reading($xml);
284 $question->questiontext .= $this->hotpot_get_instructions($xml);
286 $question->subquestions = array();
287 $question->subanswers = array();
288 $p = 0;
289 while (($pair = $exercise."['pair'][$p]['#']") && $xml->xml_value($tags, $pair)) {
290 $left = $xml->xml_value($tags, $pair."['left-item'][0]['#']['text'][0]['#']");
291 $right = $xml->xml_value($tags, $pair."['right-item'][0]['#']['text'][0]['#']");
292 if ($left && $right) {
293 $match_count++;
294 $question->subquestions[$p] = $this->hotpot_prepare_str($left);
295 $question->subanswers[$p] = $this->hotpot_prepare_str($right);
297 $p++;
299 $question->defaultgrade = $match_count * $defaultgrade;
300 $questions[] = $question;
301 $x++;
305 function process_jmix(&$xml, &$questions) {
306 // define default grade (per segment)
307 $defaultgrade = 1;
308 $segment_count = 0;
310 // xml tags to the start of the jumbled order exercise
311 $tags = 'data,jumbled-order-exercise';
313 $x = 0;
314 while (($exercise = "[$x]['#']") && $xml->xml_value($tags, $exercise)) {
315 // there is usually only one exercise in a file
317 if (method_exists($this, 'defaultquestion')) {
318 $question = $this->defaultquestion();
319 } else {
320 $question = new stdClass();
321 $question->usecase = 0; // Ignore case
322 $question->image = ""; // No images with this format
324 $question->qtype = SHORTANSWER;
325 $question->name = $this->hotpot_get_title($xml, $x);
327 $question->answer = array();
328 $question->fraction = array();
329 $question->feedback = array();
331 $i = 0;
332 $segments = array();
333 while ($segment = $xml->xml_value($tags, $exercise."['main-order'][0]['#']['segment'][$i]['#']")) {
334 $segments[] = $this->hotpot_prepare_str($segment);
335 $segment_count++;
336 $i++;
338 $answer = implode(' ', $segments);
340 $this->hotpot_seed_RNG();
341 shuffle($segments);
343 $question->questiontext = $this->hotpot_get_reading($xml);
344 $question->questiontext .= $this->hotpot_get_instructions($xml);
345 $question->questiontext .= ' &nbsp; <NOBR><B>[ &nbsp; '.implode(' &nbsp; ', $segments).' &nbsp; ]</B></NOBR>';
347 $a = 0;
348 while (!empty($answer)) {
349 $question->answer[$a] = $answer;
350 $question->fraction[$a] = 1;
351 $question->feedback[$a] = '';
352 $answer = $this->hotpot_prepare_str($xml->xml_value($tags, $exercise."['alternate'][$a]['#']"));
353 $a++;
355 $question->defaultgrade = $segment_count * $defaultgrade;
356 $questions[] = $question;
357 $x++;
360 function process_jquiz(&$xml, &$questions) {
361 // define default grade (per question)
362 $defaultgrade = 1;
364 // xml tags to the start of the questions
365 $tags = 'data,questions';
367 $x = 0;
368 while (($exercise = "[$x]['#']") && $xml->xml_value($tags, $exercise)) {
369 // there is usually only one 'questions' object in a single exercise
371 $q = 0;
372 while (($question_record = $exercise."['question-record'][$q]['#']") && $xml->xml_value($tags, $question_record)) {
374 if (method_exists($this, 'defaultquestion')) {
375 $question = $this->defaultquestion();
376 } else {
377 $question = new stdClass();
378 $question->usecase = 0; // Ignore case
379 $question->image = ""; // No images with this format
381 $question->defaultgrade = $defaultgrade;
382 $question->name = $this->hotpot_get_title($xml, $q, true);
384 $text = $xml->xml_value($tags, $question_record."['question'][0]['#']");
385 $question->questiontext = $this->hotpot_prepare_str($text);
387 if ($xml->xml_value($tags, $question_record."['answers']")) {
388 // HP6 JQuiz
389 $answers = $question_record."['answers'][0]['#']";
390 } else {
391 // HP5 JBC or JQuiz
392 $answers = $question_record;
394 if($xml->xml_value($tags, $question_record."['question-type']")) {
395 // HP6 JQuiz
396 $type = $xml->xml_value($tags, $question_record."['question-type'][0]['#']");
397 // 1 : multiple choice
398 // 2 : short-answer
399 // 3 : hybrid
400 // 4 : multiple select
401 } else {
402 // HP5
403 switch ($xml->quiztype) {
404 case 'jbc':
405 $must_select_all = $xml->xml_value($tags, $question_record."['must-select-all'][0]['#']");
406 if (empty($must_select_all)) {
407 $type = 1; // multichoice
408 } else {
409 $type = 4; // multiselect
411 break;
412 case 'jquiz':
413 $type = 2; // shortanswer
414 break;
415 default:
416 $type = 0; // unknown
419 $question->qtype = ($type==2 ? SHORTANSWER : MULTICHOICE);
420 $question->single = ($type==4 ? 0 : 1);
422 // workaround required to calculate scores for multiple select answers
423 $no_of_correct_answers = 0;
424 if ($type==4) {
425 $a = 0;
426 while (($answer = $answers."['answer'][$a]['#']") && $xml->xml_value($tags, $answer)) {
427 $correct = $xml->xml_value($tags, $answer."['correct'][0]['#']");
428 if (empty($correct)) {
429 // do nothing
430 } else {
431 $no_of_correct_answers++;
433 $a++;
436 $a = 0;
437 $question->answer = array();
438 $question->fraction = array();
439 $question->feedback = array();
440 $aa = 0;
441 $correct_answers = array();
442 $correct_answers_all_zero = true;
443 while (($answer = $answers."['answer'][$a]['#']") && $xml->xml_value($tags, $answer)) {
444 $correct = $xml->xml_value($tags, $answer."['correct'][0]['#']");
445 if (empty($correct)) {
446 $fraction = 0;
447 } else if ($type==4) { // multiple select
448 // strange behavior if the $fraction isn't exact to 5 decimal places
449 $fraction = round(1/$no_of_correct_answers, 5);
450 } else {
451 if ($xml->xml_value($tags, $answer."['percent-correct']")) {
452 // HP6 JQuiz
453 $percent = $xml->xml_value($tags, $answer."['percent-correct'][0]['#']");
454 $fraction = $percent/100;
455 } else {
456 // HP5 JBC or JQuiz
457 $fraction = 1;
460 $answertext = $this->hotpot_prepare_str($xml->xml_value($tags, $answer."['text'][0]['#']"));
461 if ($answertext!='') {
462 $question->answer[$aa] = $answertext;
463 $question->fraction[$aa] = $fraction;
464 $question->feedback[$aa] = $this->hotpot_prepare_str($xml->xml_value($tags, $answer."['feedback'][0]['#']"));
465 if ($correct) {
466 if ($fraction) {
467 $correct_answers_all_zero = false;
469 $correct_answers[] = $aa;
471 $aa++;
473 $a++;
475 if ($correct_answers_all_zero) {
476 // correct answers all have score of 0%,
477 // so reset score for correct answers 100%
478 foreach ($correct_answers as $aa) {
479 $question->fraction[$aa] = 1;
482 $questions[] = $question;
483 $q++;
485 $x++;
489 function hotpot_seed_RNG() {
490 // seed the random number generator
491 static $HOTPOT_SEEDED_RNG = FALSE;
492 if (!$HOTPOT_SEEDED_RNG) {
493 srand((double) microtime() * 1000000);
494 $HOTPOT_SEEDED_RNG = TRUE;
497 function hotpot_get_title(&$xml, $x, $flag=false) {
498 $title = $xml->xml_value('data,title');
499 if ($x || $flag) {
500 $title .= ' ('.($x+1).')';
502 return $this->hotpot_prepare_str($title);
504 function hotpot_get_instructions(&$xml) {
505 $text = $xml->xml_value('hotpot-config-file,instructions');
506 if (empty($text)) {
507 $text = "Hot Potatoes $xml->quiztype";
509 return $this->hotpot_prepare_str($text);
511 function hotpot_get_reading(&$xml) {
512 $str = '';
513 $tags = 'data,reading';
514 if ($xml->xml_value("$tags,include-reading")) {
515 if ($title = $xml->xml_value("$tags,reading-title")) {
516 $str .= "<H3>$title</H3>";
518 if ($text = $xml->xml_value("$tags,reading-text")) {
519 $str .= "<P>$text</P>";
522 return $this->hotpot_prepare_str($str);
524 function hotpot_prepare_str($str) {
525 // convert html entities to unicode and add slashes
526 $str = preg_replace('/&#x([0-9a-f]+);/ie', "hotpot_charcode_to_utf8(hexdec('\\1'))", $str);
527 $str = preg_replace('/&#([0-9]+);/e', "hotpot_charcode_to_utf8(\\1)", $str);
528 return addslashes($str);
530 } // end class
532 // get the standard XML parser supplied with Moodle
533 require_once("$CFG->libdir/xmlize.php");
535 class hotpot_xml_tree {
536 function hotpot_xml_tree($str, $xml_root='') {
537 if (empty($str)) {
538 $this->xml = array();
539 } else {
540 // encode htmlentities in JCloze
541 $this->encode_cdata($str, 'gap-fill');
542 // xmlize (=convert xml to tree)
543 $this->xml = xmlize($str, 0);
545 $this->xml_root = $xml_root;
547 function xml_value($tags, $more_tags="[0]['#']") {
549 $tags = empty($tags) ? '' : "['".str_replace(",", "'][0]['#']['", $tags)."']";
550 eval('$value = &$this->xml'.$this->xml_root.$tags.$more_tags.';');
552 if (is_string($value)) {
554 // decode angle brackets and ampersands
555 $value = strtr($value, array('&#x003C;'=>'<', '&#x003E;'=>'>', '&#x0026;'=>'&'));
557 // remove white space between <table>, <ul|OL|DL> and <OBJECT|EMBED> parts
558 // (so it doesn't get converted to <br />)
559 $htmltags = '('
560 . 'TABLE|/?CAPTION|/?COL|/?COLGROUP|/?TBODY|/?TFOOT|/?THEAD|/?TD|/?TH|/?TR'
561 . '|OL|UL|/?LI'
562 . '|DL|/?DT|/?DD'
563 . '|EMBED|OBJECT|APPLET|/?PARAM'
564 //. '|SELECT|/?OPTION'
565 //. '|FIELDSET|/?LEGEND'
566 //. '|FRAMESET|/?FRAME'
567 . ')'
569 $search = '#(<'.$htmltags.'[^>]*'.'>)\s+'.'(?='.'<'.')#is';
570 $value = preg_replace($search, '\\1', $value);
572 // replace remaining newlines with <br />
573 $value = str_replace("\n", '<br />', $value);
575 // encode unicode characters as HTML entities
576 // (in particular, accented charaters that have not been encoded by HP)
578 // multibyte unicode characters can be detected by checking the hex value of the first character
579 // 00 - 7F : ascii char (roman alphabet + punctuation)
580 // 80 - BF : byte 2, 3 or 4 of a unicode char
581 // C0 - DF : 1st byte of 2-byte char
582 // E0 - EF : 1st byte of 3-byte char
583 // F0 - FF : 1st byte of 4-byte char
584 // if the string doesn't match the above, it might be
585 // 80 - FF : single-byte, non-ascii char
586 $search = '#('.'[\xc0-\xdf][\x80-\xbf]'.'|'.'[\xe0-\xef][\x80-\xbf]{2}'.'|'.'[\xf0-\xff][\x80-\xbf]{3}'.'|'.'[\x80-\xff]'.')#se';
587 $value = preg_replace($search, "hotpot_utf8_to_html_entity('\\1')", $value);
589 return $value;
591 function encode_cdata(&$str, $tag) {
593 // conversion tables
594 static $HTML_ENTITIES = array(
595 '&apos;' => "'",
596 '&quot;' => '"',
597 '&lt;' => '<',
598 '&gt;' => '>',
599 '&amp;' => '&',
601 static $ILLEGAL_STRINGS = array(
602 "\r" => '',
603 "\n" => '&lt;br /&gt;',
604 ']]>' => '&#93;&#93;&#62;',
607 // extract the $tag from the $str(ing), if possible
608 $pattern = '|(^.*<'.$tag.'[^>]*)(>.*<)(/'.$tag.'>.*$)|is';
609 if (preg_match($pattern, $str, $matches)) {
611 // encode problematic CDATA chars and strings
612 $matches[2] = strtr($matches[2], $ILLEGAL_STRINGS);
615 // if there are any ampersands in "open text"
616 // surround them by CDATA start and end markers
617 // (and convert HTML entities to plain text)
618 $search = '/>([^<]*&[^<]*)</e';
619 $replace = '"><![CDATA[".strtr("$1", $HTML_ENTITIES)."]]><"';
620 $matches[2] = preg_replace($search, $replace, $matches[2]);
622 $str = $matches[1].$matches[2].$matches[3];
627 function hotpot_charcode_to_utf8($charcode) {
628 if ($charcode <= 0x7F) {
629 // ascii char (roman alphabet + punctuation)
630 return chr($charcode);
632 if ($charcode <= 0x7FF) {
633 // 2-byte char
634 return chr(($charcode >> 0x06) + 0xC0).chr(($charcode & 0x3F) + 128);
636 if ($charcode <= 0xFFFF) {
637 // 3-byte char
638 return chr(($charcode >> 0x0C) + 0xE0).chr((($charcode >> 0x06) & 0x3F) + 0x80).chr(($charcode & 0x3F) + 0x80);
640 if ($charcode <= 0x1FFFFF) {
641 // 4-byte char
642 return chr(($charcode >> 0x12) + 0xF0).chr((($charcode >> 0x0C) & 0x3F) + 0x80).chr((($charcode >> 0x06) & 0x3F) + 0x80).chr(($charcode & 0x3F) + 0x80);
644 // unidentified char code !!
645 return ' ';
648 function hotpot_utf8_to_html_entity($char) {
649 // http://www.zend.com/codex.php?id=835&single=1
651 // array used to figure what number to decrement from character order value
652 // according to number of characters used to map unicode to ascii by utf-8
653 static $HOTPOT_UTF8_DECREMENT = array(
654 1=>0, 2=>192, 3=>224, 4=>240
657 // the number of bits to shift each character by
658 static $HOTPOT_UTF8_SHIFT = array(
659 1=>array(0=>0),
660 2=>array(0=>6, 1=>0),
661 3=>array(0=>12, 1=>6, 2=>0),
662 4=>array(0=>18, 1=>12, 2=>6, 3=>0)
665 $dec = 0;
666 $len = strlen($char);
667 for ($pos=0; $pos<$len; $pos++) {
668 $ord = ord ($char{$pos});
669 $ord -= ($pos ? 128 : $HOTPOT_UTF8_DECREMENT[$len]);
670 $dec += ($ord << $HOTPOT_UTF8_SHIFT[$len][$pos]);
672 return '&#x'.sprintf('%04X', $dec).';';
675 function hotpot_convert_relative_urls($str, $baseurl, $filename) {
676 $tagopen = '(?:(<)|(&lt;)|(&amp;#x003C;))'; // left angle bracket
677 $tagclose = '(?(2)>|(?(3)&gt;|(?(4)&amp;#x003E;)))'; // right angle bracket (to match left angle bracket)
679 $space = '\s+'; // at least one space
680 $anychar = '(?:[^>]*?)'; // any character
682 $quoteopen = '("|&quot;|&amp;quot;)'; // open quote
683 $quoteclose = '\\5'; // close quote (to match open quote)
685 $replace = "hotpot_convert_relative_url('".$baseurl."', '".$filename."', '\\1', '\\6', '\\7')";
687 $tags = array('script'=>'src', 'link'=>'href', 'a'=>'href','img'=>'src','param'=>'value', 'object'=>'data', 'embed'=>'src');
688 foreach ($tags as $tag=>$attribute) {
689 if ($tag=='param') {
690 $url = '\S+?\.\S+?'; // must include a filename and have no spaces
691 } else {
692 $url = '.*?';
694 $search = "%($tagopen$tag$space$anychar$attribute=$quoteopen)($url)($quoteclose$anychar$tagclose)%ise";
695 $str = preg_replace($search, $replace, $str);
698 return $str;
701 function hotpot_convert_relative_url($baseurl, $filename, $opentag, $url, $closetag, $stripslashes=true) {
702 if ($stripslashes) {
703 $opentag = stripslashes($opentag);
704 $url = stripslashes($url);
705 $closetag = stripslashes($closetag);
708 // catch <PARAM name="FlashVars" value="TheSound=soundfile.mp3">
709 // ampersands can appear as "&", "&amp;" or "&amp;#x0026;amp;"
710 if (preg_match('|^'.'\w+=[^&]+'.'('.'&((amp;#x0026;)?amp;)?'.'\w+=[^&]+)*'.'$|', $url)) {
711 $query = $url;
712 $url = '';
713 $fragment = '';
715 // parse the $url into $matches
716 // [1] path
717 // [2] query string, if any
718 // [3] anchor fragment, if any
719 } else if (preg_match('|^'.'([^?]*)'.'((?:\\?[^#]*)?)'.'((?:#.*)?)'.'$|', $url, $matches)) {
720 $url = $matches[1];
721 $query = $matches[2];
722 $fragment = $matches[3];
724 // there appears to be no query or fragment in this url
725 } else {
726 $query = '';
727 $fragment = '';
730 if ($url) {
731 $url = hotpot_convert_url($baseurl, $filename, $url, false);
734 if ($query) {
735 $search = '#'.'(file|src|thesound)='."([^&]+)".'#ise';
736 $replace = "'\\1='.hotpot_convert_url('".$baseurl."','".$filename."','\\2')";
737 $query = preg_replace($search, $replace, $query);
740 $url = $opentag.$url.$query.$fragment.$closetag;
742 return $url;
745 function hotpot_convert_url($baseurl, $filename, $url, $stripslashes=true) {
746 // maintain a cache of converted urls
747 static $HOTPOT_RELATIVE_URLS = array();
749 if ($stripslashes) {
750 $url = stripslashes($url);
753 // is this an absolute url? (or javascript pseudo url)
754 if (preg_match('%^(http://|/|javascript:)%i', $url)) {
755 // do nothing
757 // has this relative url already been converted?
758 } else if (isset($HOTPOT_RELATIVE_URLS[$url])) {
759 $url = $HOTPOT_RELATIVE_URLS[$url];
761 } else {
762 $relativeurl = $url;
764 // get the subdirectory, $dir, of the quiz $filename
765 $dir = dirname($filename);
767 // allow for leading "./" and "../"
768 while (preg_match('|^(\.{1,2})/(.*)$|', $url, $matches)) {
769 if ($matches[1]=='..') {
770 $dir = dirname($dir);
772 $url = $matches[2];
775 // add subdirectory, $dir, to $baseurl, if necessary
776 if ($dir && $dir<>'.') {
777 $baseurl .= "$dir/";
780 // prefix $url with $baseurl
781 $url = "$baseurl$url";
783 // add url to cache
784 $HOTPOT_RELATIVE_URLS[$relativeurl] = $url;
786 return $url;
789 // allow importing in Moodle v1.4 (and less)
790 // same core functions but different class name
791 if (!class_exists("quiz_file_format")) {
792 class quiz_file_format extends qformat_default {
793 function readquestions ($lines) {
794 $format = new qformat_hotpot();
795 return $format->readquestions($lines);