PageRenderTime 76ms CodeModel.GetById 10ms app.highlight 58ms RepoModel.GetById 1ms app.codeStats 1ms

/question/format/hotpot/format.php

https://bitbucket.org/ceu/moodle_demo
PHP | 719 lines | 561 code | 74 blank | 84 comment | 128 complexity | 809b15677814f4273a4129bdd1540571 MD5 | raw file
  1<?PHP // $Id: format.php,v 1.12.4.18 2012/05/19 11:04:54 moodlerobot Exp $
  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////////////////////////////////////////////////////////////////////////////
  9
 10// Based on default.php, included by ../import.php
 11/**
 12 * @package questionbank
 13 * @subpackage importexport
 14 */
 15require_once($CFG->dirroot . '/mod/hotpot/lib.php');
 16
 17class qformat_hotpot extends qformat_default {
 18
 19    function provide_import() {
 20        return true;
 21    }
 22
 23    function readquestions ($lines) {
 24        /// Parses an array of lines into an array of questions,
 25        /// where each item is a question object as defined by
 26        /// readquestion().
 27
 28        // set courseid and baseurl
 29        global $CFG, $COURSE, $course;
 30        switch (true) {
 31            case isset($this->course->id):
 32                // import to quiz module
 33                $courseid = $this->course->id;
 34                break;
 35            case isset($course->id):
 36                // import to lesson module
 37                $courseid = $course->id;
 38                break;
 39            case isset($COURSE->id):
 40                // last resort
 41                $courseid = $COURSE->id;
 42                break;
 43            default:
 44                // shouldn't happen !!
 45                $courseid = 0;
 46        }
 47        require_once($CFG->libdir.'/filelib.php');
 48        $baseurl = get_file_url($courseid).'/';
 49
 50        // get import file name
 51        global $params;
 52        if (! empty($this->realfilename)) {
 53            $filename = $this->realfilename;
 54        } else 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']);
 60        }
 61
 62        // get hotpot file source
 63        $source = implode($lines, " ");
 64        $source = hotpot_convert_relative_urls($source, $baseurl, $filename);
 65
 66        // create xml tree for this hotpot
 67        $xml = new hotpot_xml_tree($source);
 68
 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;
 77            }
 78        }
 79
 80        // convert xml to questions array
 81        $questions = array();
 82        switch ($xml->quiztype) {
 83            case 'jcloze':
 84                if (strpos($source, '<gap-fill><question-record>')) {
 85                    $startwithgap = true;
 86                } else {
 87                    $startwithgap = false;
 88                }
 89                $this->process_jcloze($xml, $questions, $startwithgap);
 90                break;
 91            case 'jcross':
 92                $this->process_jcross($xml, $questions);
 93                break;
 94            case 'jmatch':
 95                $this->process_jmatch($xml, $questions);
 96                break;
 97            case 'jmix':
 98                $this->process_jmix($xml, $questions);
 99                break;
100            case 'jbc':
101            case 'jquiz':
102                $this->process_jquiz($xml, $questions);
103                break;
104            default:
105                if (empty($xml->quiztype)) {
106                    notice("Input file not recognized as a Hot Potatoes XML file");
107                } else {
108                    notice("Unknown quiz type '$xml->quiztype'");
109                }
110        } // end switch
111
112        if (count($questions)) {
113            return $questions;
114        } else {
115            if (method_exists($this, 'error')) { // Moodle >= 1.8
116                $this->error(get_string('giftnovalidquestion', 'quiz'));
117            }
118            return false;
119        }
120    }
121
122    function process_jcloze(&$xml, &$questions, $startwithgap) {
123        // define default grade (per cloze gap)
124        $defaultgrade = 1;
125        $gap_count = 0;
126
127        // detect old Moodles (1.4 and earlier)
128        global $CFG, $db;
129        $moodle_14 = false;
130        if ($columns = $db->MetaColumns("{$CFG->prefix}question_multianswer")) {
131            foreach ($columns as $column) {
132                if ($column->name=='answers' || $column->name=='positionkey' || $column->name=='answertype' || $column->name=='norm') {
133                    $moodle_14 = true;
134                }
135            }
136        }
137
138        // xml tags for the start of the gap-fill exercise
139        $tags = 'data,gap-fill';
140
141        $x = 0;
142        while (($exercise = "[$x]['#']") && $xml->xml_value($tags, $exercise)) {
143            // there is usually only one exercise in a file
144
145            if (method_exists($this, 'defaultquestion')) {
146                $question = $this->defaultquestion();
147            } else {
148                $question = new stdClass();
149                $question->usecase = 0; // Ignore case
150                $question->image = "";  // No images with this format
151            }
152
153            $question->qtype = MULTIANSWER;
154            $question->name = $this->hotpot_get_title($xml, $x);
155            $question->questiontext = '';
156
157            // add get dropdown list, if any
158            if (intval($xml->xml_value('hotpot-config-file,'.$xml->quiztype.',use-drop-down-list'))) {
159                $dropdownlist = $this->hotpot_jcloze_wordlist($xml);
160                $answertype = MULTICHOICE;
161            } else {
162                $dropdownlist = false;
163                $answertype = SHORTANSWER;
164
165                // add wordlist, if required (not required if we are using dropdowns)
166                if (intval($xml->xml_value('hotpot-config-file,'.$xml->quiztype.',include-word-list'))) {
167                    $question->questiontext .= '<p>'.implode(' ', $this->hotpot_jcloze_wordlist($xml)).'</p>';
168                }
169            }
170
171            // add reading, if any
172            $question->questiontext .= $this->hotpot_get_reading($xml);
173
174            // setup answer arrays
175            if ($moodle_14) {
176                $question->answers = array();
177            } else {
178                global $COURSE; // initialized in questions/import.php
179                $question->course = $COURSE->id;
180                $question->options = new stdClass();
181                $question->options->questions = array(); // one for each gap
182            }
183
184            $q = 0;
185            $looping = true;
186            while ($looping) {
187                // get next bit of text
188                $questiontext = $xml->xml_value($tags, $exercise."[$q]");
189                $questiontext = $this->hotpot_prepare_str($questiontext);
190
191                // get next gap
192                $gap = '';
193                $question_record = $exercise."['question-record'][$q]['#']";
194                if ($xml->xml_value($tags, $question_record)) {
195
196                    // add gap
197                    $gap_count ++;
198                    $positionkey = $q+1;
199                    $gap = '{#'.$positionkey.'}';
200
201                    // initialize answer settings
202                    if ($moodle_14) {
203                        $question->answers[$q]->positionkey = $positionkey;
204                        $question->answers[$q]->answertype = $answertype;
205                        $question->answers[$q]->norm = $defaultgrade;
206                        $question->answers[$q]->alternatives = array();
207                    } else {
208                        $wrapped = new stdClass();
209                        $wrapped->qtype = $answertype;
210                        $wrapped->usecase = 0;
211                        $wrapped->defaultgrade = $defaultgrade;
212                        $wrapped->questiontextformat = 0;
213                        $wrapped->answer = array();
214                        $wrapped->fraction = array();
215                        $wrapped->feedback = array();
216                        // required for multichoice
217                        $wrapped->single = 1;
218                        $wrapped->answernumbering = 0;
219                        $wrapped->shuffleanswers = 0;
220                        $wrapped->correctfeedback = '';
221                        $wrapped->partiallycorrectfeedback = '';
222                        $wrapped->incorrectfeedback = '';
223                        // array of answers
224                        $answers = array();
225                    }
226
227                    // add answers
228                    if ($dropdownlist) {
229
230                        $a = 0;
231                        $correcttext = '';
232                        $correctfeedback = '';
233                        while (($answer=$question_record."['answer'][$a]['#']") && $xml->xml_value($tags, $answer)) {
234                            if (intval($xml->xml_value($tags,  $answer."['correct'][0]['#']"))) {
235                                $correcttext = $this->hotpot_prepare_str($xml->xml_value($tags,  $answer."['text'][0]['#']"));
236                                $correctfeedback = $this->hotpot_prepare_str($xml->xml_value($tags,  $answer."['feedback'][0]['#']"));
237                                break;
238                            }
239                            $a++;
240                        }
241
242                        foreach ($dropdownlist as $a => $answer) {
243                            if ($answer==$correcttext) {
244                                $fraction = 1;
245                                $feedback = $correctfeedback;
246                            } else {
247                                $fraction = 0;
248                                $feedback = '';
249                            }
250                            if ($moodle_14) {
251                                $question->answers[$q]->alternatives[$a] = new stdClass();
252                                $question->answers[$q]->alternatives[$a]->answer = $answer;
253                                $question->answers[$q]->alternatives[$a]->fraction = $fraction;
254                                $question->answers[$q]->alternatives[$a]->feedback = $feedback;
255                            } else {
256                                $wrapped->answer[] = $answer;
257                                $wrapped->fraction[] = $fraction;
258                                $wrapped->feedback[] = $feedback;
259                                $answers[] = ($fraction==0 ? '' : '=').$answer.($feedback=='' ? '' : ('#'.$feedback));
260                            }
261                        }
262                    } else {
263                        $a = 0;
264                        while (($answer=$question_record."['answer'][$a]['#']") && $xml->xml_value($tags, $answer)) {
265                            $text = $this->hotpot_prepare_str($xml->xml_value($tags,  $answer."['text'][0]['#']"));
266                            $correct = $xml->xml_value($tags,  $answer."['correct'][0]['#']");
267                            $feedback = $this->hotpot_prepare_str($xml->xml_value($tags,  $answer."['feedback'][0]['#']"));
268                            if (strlen($text)) {
269                                // set score (0=0%, 1=100%)
270                                $fraction = empty($correct) ? 0 : 1;
271                                // store answer
272                                if ($moodle_14) {
273                                    $question->answers[$q]->alternatives[$a] = new stdClass();
274                                    $question->answers[$q]->alternatives[$a]->answer = $text;
275                                    $question->answers[$q]->alternatives[$a]->fraction = $fraction;
276                                    $question->answers[$q]->alternatives[$a]->feedback = $feedback;
277                                } else {
278                                    $wrapped->answer[] = $text;
279                                    $wrapped->fraction[] = $fraction;
280                                    $wrapped->feedback[] = $feedback;
281                                    $answers[] = (empty($fraction) ? '' : '=').$text.(empty($feedback) ? '' : ('#'.$feedback));
282                                }
283                            }
284                            $a++;
285                        }
286                    }
287
288                    // compile answers into question text, if necessary
289                    if ($moodle_14) {
290                        // do nothing
291                    } else {
292                        $wrapped->questiontext = '{'.$defaultgrade.':'.$answertype.':'.implode('~', $answers).'}';
293                        $question->options->questions[] = $wrapped;
294                    }
295                } // end if gap
296
297                if (strlen($questiontext) || strlen($gap)) {
298                    if ($startwithgap) {
299                        $question->questiontext .= $gap.$questiontext;
300                    } else {
301                        $question->questiontext .= $questiontext.$gap;
302                    }
303                } else {
304                    $looping = false;
305                }
306
307                $q++;
308            } // end while $looping
309
310            if ($q) {
311                // define total grade for this exercise
312                $question->defaultgrade = $gap_count * $defaultgrade;
313
314                // add this cloze as a single question object
315                $questions[] = $question;
316            } else {
317                // no gaps found in this text so don't add this question
318                // import will fail and error message will be displayed:
319            }
320
321            $x++;
322        } // end while $exercise
323    }
324
325    function hotpot_jcloze_wordlist(&$xml) {
326        $wordlist = array();
327
328        $q = 0;
329        $tags = 'data,gap-fill,question-record';
330        while (($question="[$q]['#']") && $xml->xml_value($tags, $question)) {
331            $a = 0;
332            $aa = 0;
333            while (($answer=$question."['answer'][$a]['#']") && $xml->xml_value($tags, $answer)) {
334                $text = $xml->xml_value($tags,  $answer."['text'][0]['#']");
335                $correct = $xml->xml_value($tags, $answer."['correct'][0]['#']");
336                if (strlen($text) && intval($correct)) {
337                    $wordlist[] = $text;
338                    $aa++;
339                }
340                $a++;
341            }
342            $q++;
343        }
344
345        $wordlist = array_unique($wordlist);
346        sort($wordlist);
347
348        return $wordlist;
349    }
350
351    function process_jcross(&$xml, &$questions) {
352        // xml tags to the start of the crossword exercise clue items
353        $tags = 'data,crossword,clues,item';
354
355        $x = 0;
356        while (($item = "[$x]['#']") && $xml->xml_value($tags, $item)) {
357
358            $text = $xml->xml_value($tags, $item."['def'][0]['#']");
359            $answer = $xml->xml_value($tags, $item."['word'][0]['#']");
360
361            if ($text && $answer) {
362                if (method_exists($this, 'defaultquestion')) {
363                    $question = $this->defaultquestion();
364                } else {
365                    $question = new stdClass();
366                    $question->usecase = 0; // Ignore case
367                    $question->image = "";  // No images with this format
368                }
369                $question->qtype = SHORTANSWER;
370                $question->name = $this->hotpot_get_title($xml, $x, true);
371
372                $question->questiontext = $this->hotpot_prepare_str($text);
373                $question->answer = array($this->hotpot_prepare_str($answer));
374                $question->fraction = array(1);
375                $question->feedback = array('');
376
377                $questions[] = $question;
378            }
379            $x++;
380        }
381    }
382
383    function process_jmatch(&$xml, &$questions) {
384        // define default grade (per matched pair)
385        $defaultgrade = 1;
386        $match_count = 0;
387
388        // xml tags to the start of the matching exercise
389        $tags = 'data,matching-exercise';
390
391        $x = 0;
392        while (($exercise = "[$x]['#']") && $xml->xml_value($tags, $exercise)) {
393            // there is usually only one exercise in a file
394
395            if (method_exists($this, 'defaultquestion')) {
396                $question = $this->defaultquestion();
397            } else {
398                $question = new stdClass();
399                $question->usecase = 0; // Ignore case
400                $question->image = "";  // No images with this format
401            }
402            $question->qtype = MATCH;
403            $question->name = $this->hotpot_get_title($xml, $x);
404
405            $question->questiontext = $this->hotpot_get_reading($xml);
406            $question->questiontext .= $this->hotpot_get_instructions($xml);
407
408            $question->subquestions = array();
409            $question->subanswers = array();
410            $p = 0;
411            while (($pair = $exercise."['pair'][$p]['#']") && $xml->xml_value($tags, $pair)) {
412                $left = $xml->xml_value($tags, $pair."['left-item'][0]['#']['text'][0]['#']");
413                $right = $xml->xml_value($tags, $pair."['right-item'][0]['#']['text'][0]['#']");
414                if ($left && $right) {
415                    $match_count++;
416                    $question->subquestions[$p] = $this->hotpot_prepare_str($left);
417                    $question->subanswers[$p] = $this->hotpot_prepare_str($right);
418                }
419                $p++;
420            }
421            $question->defaultgrade = $match_count * $defaultgrade;
422            $questions[] = $question;
423            $x++;
424        }
425    }
426
427    function process_jmix(&$xml, &$questions) {
428        // define default grade (per segment)
429        $defaultgrade = 1;
430        $segment_count = 0;
431
432        // xml tags to the start of the jumbled order exercise
433        $tags = 'data,jumbled-order-exercise';
434
435        $x = 0;
436        while (($exercise = "[$x]['#']") && $xml->xml_value($tags, $exercise)) {
437            // there is usually only one exercise in a file
438
439            if (method_exists($this, 'defaultquestion')) {
440                $question = $this->defaultquestion();
441            } else {
442                $question = new stdClass();
443                $question->usecase = 0; // Ignore case
444                $question->image = "";  // No images with this format
445            }
446            $question->qtype = SHORTANSWER;
447            $question->name = $this->hotpot_get_title($xml, $x);
448
449            $question->answer = array();
450            $question->fraction = array();
451            $question->feedback = array();
452
453            $i = 0;
454            $segments = array();
455            while ($segment = $xml->xml_value($tags, $exercise."['main-order'][0]['#']['segment'][$i]['#']")) {
456                $segments[] = $this->hotpot_prepare_str($segment);
457                $segment_count++;
458                $i++;
459            }
460            $answer = implode(' ', $segments);
461
462            $this->hotpot_seed_RNG();
463            shuffle($segments);
464
465            $question->questiontext = $this->hotpot_get_reading($xml);
466            $question->questiontext .= $this->hotpot_get_instructions($xml);
467            $question->questiontext .= ' &nbsp; <NOBR><B>[ &nbsp; '.implode(' &nbsp; ', $segments).' &nbsp; ]</B></NOBR>';
468
469            $a = 0;
470            while (!empty($answer)) {
471                $question->answer[$a] = $answer;
472                $question->fraction[$a] = 1;
473                $question->feedback[$a] = '';
474                $answer = $this->hotpot_prepare_str($xml->xml_value($tags, $exercise."['alternate'][$a]['#']"));
475                $a++;
476            }
477            $question->defaultgrade = $segment_count * $defaultgrade;
478            $questions[] = $question;
479            $x++;
480        }
481    }
482    function process_jquiz(&$xml, &$questions) {
483        // define default grade (per question)
484        $defaultgrade = 1;
485
486        // xml tags to the start of the questions
487        $tags = 'data,questions';
488
489        $x = 0;
490        while (($exercise = "[$x]['#']") && $xml->xml_value($tags, $exercise)) {
491            // there is usually only one 'questions' object in a single exercise
492
493            $q = 0;
494            while (($question_record = $exercise."['question-record'][$q]['#']") && $xml->xml_value($tags, $question_record)) {
495
496                if (method_exists($this, 'defaultquestion')) {
497                    $question = $this->defaultquestion();
498                } else {
499                    $question = new stdClass();
500                    $question->usecase = 0; // Ignore case
501                    $question->image = "";  // No images with this format
502                }
503                $question->defaultgrade = $defaultgrade;
504                $question->name = $this->hotpot_get_title($xml, $q, true);
505
506                $text = $xml->xml_value($tags, $question_record."['question'][0]['#']");
507                $question->questiontext = $this->hotpot_prepare_str($text);
508
509                if ($xml->xml_value($tags, $question_record."['answers']")) {
510                    // HP6 JQuiz
511                    $answers = $question_record."['answers'][0]['#']";
512                } else {
513                    // HP5 JBC or JQuiz
514                    $answers = $question_record;
515                }
516                if($xml->xml_value($tags, $question_record."['question-type']")) {
517                    // HP6 JQuiz
518                    $type = $xml->xml_value($tags, $question_record."['question-type'][0]['#']");
519                    //  1 : multiple choice
520                    //  2 : short-answer
521                    //  3 : hybrid
522                    //  4 : multiple select
523                } else {
524                    // HP5
525                    switch ($xml->quiztype) {
526                        case 'jbc':
527                            $must_select_all = $xml->xml_value($tags, $question_record."['must-select-all'][0]['#']");
528                            if (empty($must_select_all)) {
529                                $type = 1; // multichoice
530                            } else {
531                                $type = 4; // multiselect
532                            }
533                            break;
534                        case 'jquiz':
535                            $type = 2; // shortanswer
536                            break;
537                        default:
538                            $type = 0; // unknown
539                    }
540                }
541                $question->qtype = ($type==2 ? SHORTANSWER : MULTICHOICE);
542                $question->single = ($type==4 ? 0 : 1);
543
544                // workaround required to calculate scores for multiple select answers
545                $no_of_correct_answers = 0;
546                if ($type==4) {
547                    $a = 0;
548                    while (($answer = $answers."['answer'][$a]['#']") && $xml->xml_value($tags, $answer)) {
549                        $correct = $xml->xml_value($tags, $answer."['correct'][0]['#']");
550                        if (empty($correct)) {
551                            // do nothing
552                        } else {
553                            $no_of_correct_answers++;
554                        }
555                        $a++;
556                    }
557                }
558                $a = 0;
559                $question->answer = array();
560                $question->fraction = array();
561                $question->feedback = array();
562                $aa = 0;
563                $correct_answers = array();
564                $correct_answers_all_zero = true;
565                while (($answer = $answers."['answer'][$a]['#']") && $xml->xml_value($tags, $answer)) {
566                    $correct = $xml->xml_value($tags, $answer."['correct'][0]['#']");
567                    if (empty($correct)) {
568                        $fraction = 0;
569                    } else if ($type==4) { // multiple select
570                        // strange behavior if the $fraction isn't exact to 5 decimal places
571                        $fraction = round(1/$no_of_correct_answers, 5);
572                    } else {
573                        if ($xml->xml_value($tags, $answer."['percent-correct']")) {
574                            // HP6 JQuiz
575                            $percent = $xml->xml_value($tags, $answer."['percent-correct'][0]['#']");
576                            $fraction = $percent/100;
577                        } else {
578                            // HP5 JBC or JQuiz
579                            $fraction = 1;
580                        }
581                    }
582                    $answertext = $this->hotpot_prepare_str($xml->xml_value($tags, $answer."['text'][0]['#']"));
583                    if ($answertext!='') {
584                        $question->answer[$aa] = $answertext;
585                        $question->fraction[$aa] = $fraction;
586                        $question->feedback[$aa] = $this->hotpot_prepare_str($xml->xml_value($tags, $answer."['feedback'][0]['#']"));
587                        if ($correct) {
588                            if ($fraction) {
589                                $correct_answers_all_zero = false;
590                            }
591                            $correct_answers[] = $aa;
592                        }
593                        $aa++;
594                    }
595                    $a++;
596                }
597                if ($correct_answers_all_zero) {
598                    // correct answers all have score of 0%,
599                    // so reset score for correct answers 100%
600                    foreach ($correct_answers as $aa) {
601                        $question->fraction[$aa] = 1;
602                    }
603                }
604                // add a sanity check for empty questions, see MDL-17779
605                if (!empty($question->questiontext)) {
606                    $questions[] = $question;
607                }
608                $q++;
609            }
610            $x++;
611        }
612    }
613
614    function hotpot_seed_RNG() {
615        // seed the random number generator
616        static $HOTPOT_SEEDED_RNG = FALSE;
617        if (!$HOTPOT_SEEDED_RNG) {
618            srand((double) microtime() * 1000000);
619            $HOTPOT_SEEDED_RNG = TRUE;
620        }
621    }
622    function hotpot_get_title(&$xml, $x, $flag=false) {
623        $title = $xml->xml_value('data,title');
624        if ($x || $flag) {
625            $title .= ' ('.($x+1).')';
626        }
627        return $this->hotpot_prepare_str($title);
628    }
629    function hotpot_get_instructions(&$xml) {
630        $text = $xml->xml_value('hotpot-config-file,'.$xml->quiztype.',instructions');
631        if (empty($text)) {
632            $text = "Hot Potatoes $xml->quiztype";
633        }
634        return $this->hotpot_prepare_str($text);
635    }
636    function hotpot_get_reading(&$xml) {
637        $str = '';
638        $tags = 'data,reading';
639        if ($xml->xml_value("$tags,include-reading")) {
640            if ($title = $xml->xml_value("$tags,reading-title")) {
641                $str .= "<h3>$title</h3>";
642            }
643            if ($text = $xml->xml_value("$tags,reading-text")) {
644                $str .= "<p>$text</p>";
645            }
646        }
647        return $this->hotpot_prepare_str($str);
648    }
649    function hotpot_prepare_str($str) {
650        // convert html entities to unicode and add slashes
651        $str = preg_replace_callback('/&#([0-9]+);/', array(&$this, 'hotpot_prepare_str_dec'), $str);
652        $str = preg_replace_callback('/&#x([0-9a-f]+);/i', array(&$this, 'hotpot_prepare_str_hexdec'), $str);
653        return addslashes($str);
654    }
655    function hotpot_prepare_str_dec(&$matches) {
656        return hotpot_charcode_to_utf8($matches[1]);
657    }
658    function hotpot_prepare_str_hexdec(&$matches) {
659        return hotpot_charcode_to_utf8(hexdec($matches[1]));
660    }
661} // end class
662
663function hotpot_charcode_to_utf8($charcode) {
664    // thanks to Miguel Perez: http://jp2.php.net/chr (19-Sep-2007)
665    if ($charcode <= 0x7F) {
666        // ascii char (roman alphabet + punctuation)
667        return chr($charcode);
668    }
669    if ($charcode <= 0x7FF) {
670        // 2-byte char
671        return chr(($charcode >> 0x06) + 0xC0).chr(($charcode & 0x3F) + 0x80);
672    }
673    if ($charcode <= 0xFFFF) {
674        // 3-byte char
675        return chr(($charcode >> 0x0C) + 0xE0).chr((($charcode >> 0x06) & 0x3F) + 0x80).chr(($charcode & 0x3F) + 0x80);
676    }
677    if ($charcode <= 0x1FFFFF) {
678        // 4-byte char
679        return chr(($charcode >> 0x12) + 0xF0).chr((($charcode >> 0x0C) & 0x3F) + 0x80).chr((($charcode >> 0x06) & 0x3F) + 0x80).chr(($charcode & 0x3F) + 0x80);
680    }
681    // unidentified char code !!
682    return ' ';
683}
684
685function hotpot_convert_relative_urls($str, $baseurl, $filename) {
686    $tagopen = '(?:(<)|(&lt;)|(&amp;#x003C;))'; // left angle bracket
687    $tagclose = '(?(2)>|(?(3)&gt;|(?(4)&amp;#x003E;)))'; //  right angle bracket (to match left angle bracket)
688
689    $space = '\s+'; // at least one space
690    $anychar = '(?:[^>]*?)'; // any character
691
692    $quoteopen = '("|&quot;|&amp;quot;)'; // open quote
693    $quoteclose = '\\5'; //  close quote (to match open quote)
694
695    $replace = "hotpot_convert_relative_url('".$baseurl."', '".$filename."', '\\1', '\\6', '\\7')";
696
697    $tags = array('script'=>'src', 'link'=>'href', 'a'=>'href','img'=>'src','param'=>'value', 'object'=>'data', 'embed'=>'src');
698    foreach ($tags as $tag=>$attribute) {
699        if ($tag=='param') {
700            $url = '\S+?\.\S+?'; // must include a filename and have no spaces
701        } else {
702            $url = '.*?';
703        }
704        $search = "/($tagopen$tag$space$anychar$attribute=$quoteopen)($url)($quoteclose$anychar$tagclose)/is";
705        if (preg_match_all($search, $str, $matches, PREG_OFFSET_CAPTURE)) {
706            $i_max = count($matches[0]) - 1;
707            for ($i=$i_max; $i>=0; $i--) {
708                $match = $matches[0][$i][0];
709                $start = $matches[0][$i][1];
710                $replace = hotpot_convert_relative_url(
711                    $baseurl, $filename, $matches[1][$i][0], $matches[6][$i][0], $matches[7][$i][0], false
712                );
713                $str = substr_replace($str, $replace, $start, strlen($match));
714            }
715        }
716    }
717
718    return $str;
719}