'Authorize.net Payment Gateway' is better and shorter :)
[moodle.git] / question / format / xml / format.php
CommitLineData
84769fd8 1<?php // $Id$
2//
3///////////////////////////////////////////////////////////////
4// XML import/export
5//
6//////////////////////////////////////////////////////////////////////////
7// Based on default.php, included by ../import.php
8
9
10require_once( "$CFG->libdir/xmlize.php" );
11
f5565b69 12class qformat_xml extends qformat_default {
84769fd8 13
14 function provide_import() {
15 return true;
16 }
17
18 function provide_export() {
19 return true;
20 }
21
22 // IMPORT FUNCTIONS START HERE
23
24 function trans_format( $name ) {
25 // translate text format string to its internal code
26
27 $name = trim($name);
28
29 if ($name=='moodle_auto_format') {
30 $id = 0;
31 }
32 elseif ($name=='html') {
33 $id = 1;
34 }
35 elseif ($name=='plain_text') {
36 $id = 2;
37 }
38 elseif ($name=='wiki_like') {
39 $id = 3;
40 }
41 elseif ($name=='markdown') {
42 $id = 4;
43 }
44 else {
45 $id = 0; // or maybe warning required
46 }
47 return $id;
48 }
49
50 function trans_single( $name ) {
51 // translate single string to its internal format
52
53 $name = trim($name);
54
55 if ($name=="true") {
56 $id = 1;
57 }
58 elseif ($name=="false") {
59 $id = 0;
60 }
61 else {
62 $id = 0; // or maybe warning required
63 }
64 return $id;
65 }
66
67 function import_text( $text ) {
68 // handle xml 'text' element
69 $data = $text[0]['#'];
70 $data = html_entity_decode( $data );
71 return addslashes(trim( $data ));
72 }
73
74 function import_headers( $question ) {
75 // read bits that are common to all questions
76
77 // this routine initialises the question object
78 $name = $this->import_text( $question['#']['name'][0]['#']['text'] );
79 $qtext = $this->import_text( $question['#']['questiontext'][0]['#']['text'] );
80 $qformat = $question['#']['questiontext'][0]['@']['format'];
81 $image = $question['#']['image'][0]['#'];
d08e16b2 82 if (!empty($question['#']['image_base64'][0]['#'])) {
83 $image_base64 = stripslashes( trim( $question['#']['image_base64'][0]['#'] ) );
84 $image = $this->importimagefile( $image, $image_base64 );
85 }
84769fd8 86 $penalty = $question['#']['penalty'][0]['#'];
87
51bcdf28 88 $qo = $this->defaultquestion();
84769fd8 89 $qo->name = $name;
90 $qo->questiontext = $qtext;
91 $qo->questiontextformat = $this->trans_format( $qformat );
92 $qo->image = ((!empty($image)) ? $image : '');
93 $qo->penalty = $penalty;
94
95 return $qo;
96 }
97
98
99 function import_answer( $answer ) {
100 // import answer part of question
101
102 $fraction = $answer['@']['fraction'];
103 $text = $this->import_text( $answer['#']['text']);
104 $feedback = $this->import_text( $answer['#']['feedback'][0]['#']['text'] );
105
106 $ans = null;
107 $ans->answer = $text;
108 $ans->fraction = $fraction / 100;
109 $ans->feedback = $feedback;
110
111 return $ans;
112 }
113
114 function import_multichoice( $question ) {
115 // import multichoice type questions
116
117 // get common parts
118 $qo = $this->import_headers( $question );
119
120 // 'header' parts particular to multichoice
121 $qo->qtype = MULTICHOICE;
122 $single = $question['#']['single'][0]['#'];
123 $qo->single = $this->trans_single( $single );
124
125 // run through the answers
126 $answers = $question['#']['answer'];
127 $a_count = 0;
128 foreach ($answers as $answer) {
129 $ans = $this->import_answer( $answer );
130 $qo->answer[$a_count] = $ans->answer;
131 $qo->fraction[$a_count] = $ans->fraction;
132 $qo->feedback[$a_count] = $ans->feedback;
133 ++$a_count;
134 }
135
136 return $qo;
137 }
138
139 function import_truefalse( $question ) {
140 // import true/false type question
141
142 // get common parts
143 $qo = $this->import_headers( $question );
144
145 // 'header' parts particular to true/false
146 $qo->qtype = TRUEFALSE;
147
148 // get answer info
149 $answers = $question['#']['answer'];
150 $fraction0 = $answers[0]['@']['fraction'];
151 $feedback0 = $this->import_text($answers[0]['#']['feedback'][0]['#']['text']);
152 $fraction1 = $answers[1]['@']['fraction'];
153 $feedback1 = $this->import_text($answers[1]['#']['feedback'][0]['#']['text']);
154
155 // sort out which is true and build object accordingly
156 if ($fraction0==100) { // then 0 index is true
157 $qo->answer = 1;
158 $qo->feedbacktrue=$feedback0;
159 $qo->feedbackfalse=$feedback1;
160 }
161 else {
162 $qo->answer = 0;
163 $qo->feedbacktrue = $feedback1;
164 $qo->feedbackfalse = $feedback0;
165 }
166
167 return $qo;
168 }
169
170 function import_shortanswer( $question ) {
171 // import short answer question
172
173 // get common parts
174 $qo = $this->import_headers( $question );
175
176 // header parts particular to shortanswer
177 $qo->qtype = SHORTANSWER;
178
179 // get usecase
180 $qo->usecase = $question['#']['usecase'][0]['#'];
181
182 // run through the answers
183 $answers = $question['#']['answer'];
184 $a_count = 0;
185 foreach ($answers as $answer) {
186 $ans = $this->import_answer( $answer );
187 $qo->answer[$a_count] = $ans->answer;
188 $qo->fraction[$a_count] = $ans->fraction;
189 $qo->feedback[$a_count] = $ans->feedback;
190 ++$a_count;
191 }
192
193 return $qo;
194 }
195
196 function import_numerical( $question ) {
197 // import numerical question
198
199 // get common parts
200 $qo = $this->import_headers( $question );
201
202 // header parts particular to numerical
203 $qo->qtype = NUMERICAL;
204
205 // get answers array
206 $answers = $question['#']['answer'];
207 $qo->answer = array();
208 $qo->feedback = array();
209 $qo->fraction = array();
210 $qo->tolerance = array();
211 foreach ($answers as $answer) {
212 $qo->answer[] = $answer['#'][0];
a0d187bf 213 $qo->feedback[] = $this->import_text( $answer['#']['feedback'][0]['#']['text'] );
84769fd8 214 $qo->fraction[] = $answer['#']['fraction'][0]['#'];
215 $qo->tolerance[] = $answer['#']['tolerance'][0]['#'];
216 }
217
218 // get units array
84769fd8 219 $qo->unit = array();
a0d187bf 220 if (isset($question['#']['units'][0]['#']['unit'])) {
221 $units = $question['#']['units'][0]['#']['unit'];
222 $qo->multiplier = array();
223 foreach ($units as $unit) {
224 $qo->multiplier[] = $unit['#']['multiplier'][0]['#'];
225 $qo->unit[] = $unit['#']['unit_name'][0]['#'];
226 }
84769fd8 227 }
84769fd8 228 return $qo;
229 }
230
51bcdf28 231 function import_matching( $question ) {
232 // import matching question
233
234 // get common parts
235 $qo = $this->import_headers( $question );
236
237 // header parts particular to matching
238 $qo->qtype = MATCH;
239 $qo->shuffleanswers = $question['#']['shuffleanswers'][0]['#'];
240
241 // get subquestions
242 $subquestions = $question['#']['subquestion'];
243 $qo->subquestions = array();
244 $qo->subanswers = array();
245
246 // run through subquestions
247 foreach ($subquestions as $subquestion) {
a0d187bf 248 $qtext = $this->import_text( $subquestion['#']['text'] );
249 $atext = $this->import_text( $subquestion['#']['answer'][0]['#']['text'] );
51bcdf28 250 $qo->subquestions[] = $qtext;
251 $qo->subanswers[] = $atext;
252 }
51bcdf28 253 return $qo;
254 }
255
84769fd8 256 function readquestions($lines) {
257 // parse the array of lines into an array of questions
258 // this *could* burn memory - but it won't happen that much
259 // so fingers crossed!
260
261 // we just need it as one big string
262 $text = implode($lines, " ");
263 unset( $lines );
264
265 // this converts xml to big nasty data structure
266 // the 0 means keep white space as it is (important for markdown format)
267 // print_r it if you want to see what it looks like!
268 $xml = xmlize( $text, 0 );
269
270 // set up array to hold all our questions
271 $questions = array();
272
273 // iterate through questions
274 foreach ($xml['quiz']['#']['question'] as $question) {
275 $question_type = $question['@']['type'];
276 $questiontype = get_string( 'questiontype','quiz',$question_type );
c515fcf7 277 // echo "<p>$questiontype</p>";
84769fd8 278
279 if ($question_type=='multichoice') {
280 $qo = $this->import_multichoice( $question );
281 }
282 elseif ($question_type=='truefalse') {
283 $qo = $this->import_truefalse( $question );
284 }
285 elseif ($question_type=='shortanswer') {
286 $qo = $this->import_shortanswer( $question );
287 }
288 elseif ($question_type=='numerical') {
289 $qo = $this->import_numerical( $question );
290 }
51bcdf28 291 elseif ($question_type=='matching') {
292 $qo = $this->import_matching( $question );
293 }
84769fd8 294 else {
295 $notsupported = get_string( 'xmlnotsupported','quiz',$question_type );
296 echo "<p>$notsupported</p>";
297 $qo = null;
298 }
299
300 // stick the result in the $questions array
301 if ($qo) {
302 $questions[] = $qo;
303 }
304 }
305
306 return $questions;
307 }
308
309 // EXPORT FUNCTIONS START HERE
310
311 function indent_xhtml($source, $indenter = ' ') {
312 // xml tidier-upper
313 // (c) Ari Koivula http://ventionline.com
314
315 // Remove all pre-existing formatting.
316 // Remove all newlines.
317 $source = str_replace("\n", '', $source);
318 $source = str_replace("\r", '', $source);
319 // Remove all tabs.
320 $source = str_replace("\t", '', $source);
321 // Remove all space after ">" and before "<".
322 $source = ereg_replace(">( )*", ">", $source);
323 $source = ereg_replace("( )*<", "<", $source);
324
325 // Iterate through the source.
326 $level = 0;
327 $source_len = strlen($source);
328 $pt = 0;
329 while ($pt < $source_len) {
330 if ($source{$pt} === '<') {
331 // We have entered a tag.
332 // Remember the point where the tag starts.
333 $started_at = $pt;
334 $tag_level = 1;
335 // If the second letter of the tag is "/", assume its an ending tag.
336 if ($source{$pt+1} === '/') {
337 $tag_level = -1;
338 }
339 // If the second letter of the tag is "!", assume its an "invisible" tag.
340 if ($source{$pt+1} === '!') {
341 $tag_level = 0;
342 }
343 // Iterate throught the source until the end of tag.
344 while ($source{$pt} !== '>') {
345 $pt++;
346 }
347 // If the second last letter is "/", assume its a self ending tag.
348 if ($source{$pt-1} === '/') {
349 $tag_level = 0;
350 }
351 $tag_lenght = $pt+1-$started_at;
352
353 // Decide the level of indention for this tag.
354 // If this was an ending tag, decrease indent level for this tag..
355 if ($tag_level === -1) {
356 $level--;
357 }
358 // Place the tag in an array with proper indention.
359 $array[] = str_repeat($indenter, $level).substr($source, $started_at, $tag_lenght);
360 // If this was a starting tag, increase the indent level after this tag.
361 if ($tag_level === 1) {
362 $level++;
363 }
364 // if it was a self closing tag, dont do shit.
365 }
366 // Were out of the tag.
367 // If next letter exists...
368 if (($pt+1) < $source_len) {
369 // ... and its not an "<".
370 if ($source{$pt+1} !== '<') {
371 $started_at = $pt+1;
372 // Iterate through the source until the start of new tag or until we reach the end of file.
373 while ($source{$pt} !== '<' && $pt < $source_len) {
374 $pt++;
375 }
376 // If we found a "<" (we didnt find the end of file)
377 if ($source{$pt} === '<') {
378 $tag_lenght = $pt-$started_at;
379 // Place the stuff in an array with proper indention.
380 $array[] = str_repeat($indenter, $level).substr($source, $started_at, $tag_lenght);
381 }
382 // If the next tag is "<", just advance pointer and let the tag indenter take care of it.
383 } else {
384 $pt++;
385 }
386 // If the next letter doesnt exist... Were done... well, almost..
387 } else {
388 break;
389 }
390 }
391 // Replace old source with the new one we just collected into our array.
392 $source = implode($array, "\n");
393 return $source;
394 }
395
396
397 function export_file_extension() {
398 // override default type so extension is .xml
399
400 return ".xml";
401 }
402
403 function get_qtype( $type_id ) {
404 // translates question type code number into actual name
405
406 switch( $type_id ) {
407 case TRUEFALSE:
408 $name = 'truefalse';
409 break;
410 case MULTICHOICE:
411 $name = 'multichoice';
412 break;
413 case SHORTANSWER:
414 $name = 'shortanswer';
415 break;
416 case NUMERICAL:
417 $name = 'numerical';
418 break;
419 case MATCH:
420 $name = 'matching';
421 break;
422 case DESCRIPTION:
423 $name = 'description';
424 break;
425 case MULTIANSWER:
426 $name = 'cloze';
427 break;
428 default:
429 $name = 'unknown';
430 }
431 return $name;
432 }
433
434 function get_format( $id ) {
435 // translates question text format id number into something sensible
436
437 switch( $id ) {
438 case 0:
439 $name = "moodle_auto_format";
440 break;
441 case 1:
442 $name = "html";
443 break;
444 case 2:
445 $name = "plain_text";
446 break;
447 case 3:
448 $name = "wiki_like";
449 break;
450 case 4:
451 $name = "markdown";
452 break;
453 default:
454 $name = "unknown";
455 }
456 return $name;
457 }
458
459 function get_single( $id ) {
460 // translate single value into something sensible
461
462 switch( $id ) {
463 case 0:
464 $name = "false";
465 break;
466 case 1:
467 $name = "true";
468 break;
469 default:
470 $name = "unknown";
471 }
472 return $name;
473 }
474
475 function writetext( $raw, $ilev=0, $short=true) {
476 // generates <text></text> tags, processing raw text therein
477 // $ilev is the current indent level
478 // $short=true sticks it on one line
479 $indent = str_repeat( " ",$ilev );
480
481 // encode the text to 'disguise' HTML content
482 $raw = htmlspecialchars( $raw );
483
484 if ($short) {
485 $xml = "$indent<text>$raw</text>\n";
486 }
487 else {
488 $xml = "$indent<text>\n$raw\n$indent</text>\n";
489 }
490
491 return $xml;
492 }
493
494 function presave_process( $content ) {
495 // override method to allow us to add xml headers and footers
496
497 $content = "<?xml version=\"1.0\"?>\n" .
498 "<quiz>\n" .
499 $content . "\n" .
500 "</quiz>";
501
502 return $content;
503 }
504
d08e16b2 505 function writeimage( $imagepath ) {
506 // includes image in base64
507 global $CFG;
508
509 if (empty($imagepath)) {
510 return '';
511 }
512
513 $courseid = $this->course->id;
514 if (!$binary = file_get_contents( "{$CFG->dataroot}/$courseid/$imagepath" )) {
515 return '';
516 }
517
518 $content = " <image_base64>\n".addslashes(base64_encode( $binary ))."\n".
519 "\n </image_base64>\n";
520 return $content;
521 }
522
84769fd8 523 function writequestion( $question ) {
524 // turns question into string
525 // question reflects database fields for general question and specific to type
526
527 // initial string;
528 $expout = "";
529
530 // add comment
531 $expout .= "\n\n<!-- question: $question->id -->\n";
532
533 // add opening tag
534 $question_type = $this->get_qtype( $question->qtype );
535 $name_text = $this->writetext( $question->name );
536 $qtformat = $this->get_format($question->questiontextformat);
537 $question_text = $this->writetext( $question->questiontext );
538 $expout .= " <question type=\"$question_type\">\n";
539 $expout .= " <name>$name_text</name>\n";
540 $expout .= " <questiontext format=\"$qtformat\">\n";
541 $expout .= $question_text;
542 $expout .= " </questiontext>\n";
d08e16b2 543 $expout .= " <image>{$question->image}</image>\n";
544 $expout .= $this->writeimage($question->image);
84769fd8 545 $expout .= " <penalty>{$question->penalty}</penalty>\n";
546 $expout .= " <hidden>{$question->hidden}</hidden>\n";
d08e16b2 547 if (!empty($question->options->shuffleanswers)) {
548 $expout .= " <shuffleanswers>{$question->options->shuffleanswers}</shuffleanswers>\n";
549 }
550 else {
551 $expout .= " <shuffleanswers>0</shuffleanswers>\n";
552 }
84769fd8 553
554 // output depends on question type
555 switch($question->qtype) {
556 case TRUEFALSE:
36e2232e 557 foreach ($question->options->answers as $answer) {
558 $fraction_pc = round( $answer->fraction * 100 );
559 $expout .= " <answer fraction=\"$fraction_pc\">\n";
560 $expout .= $this->writetext(strtolower($answer->answer),3)."\n";
561 $expout .= " <feedback>\n";
562 $expout .= $this->writetext( $answer->feedback,4,false );
563 $expout .= " </feedback>\n";
564 $expout .= " </answer>\n";
565 }
84769fd8 566 break;
567 case MULTICHOICE:
568 $expout .= " <single>".$this->get_single($question->options->single)."</single>\n";
569 foreach($question->options->answers as $answer) {
570 $percent = $answer->fraction * 100;
571 $expout .= " <answer fraction=\"$percent\">\n";
572 $expout .= $this->writetext( $answer->answer,4,false );
573 $expout .= " <feedback>\n";
574 $expout .= $this->writetext( $answer->feedback,4,false );
575 $expout .= " </feedback>\n";
576 $expout .= " </answer>\n";
577 }
578 break;
579 case SHORTANSWER:
580 $expout .= " <usecase>{$question->options->usecase}</usecase>\n ";
581 foreach($question->options->answers as $answer) {
582 $percent = 100 * $answer->fraction;
583 $expout .= " <answer fraction=\"$percent\">\n";
584 $expout .= $this->writetext( $answer->answer,3,false );
585 $expout .= " <feedback>\n";
586 $expout .= $this->writetext( $answer->feedback,4,false );
587 $expout .= " </feedback>\n";
588 $expout .= " </answer>\n";
589 }
590 break;
591 case NUMERICAL:
592 foreach ($question->options->answers as $answer) {
593 $tolerance = $answer->tolerance;
594 $expout .= "<answer>\n";
595 $expout .= " {$answer->answer}\n";
596 $expout .= " <tolerance>$tolerance</tolerance>\n";
597 $expout .= " <feedback>".$this->writetext( $answer->feedback )."</feedback>\n";
598 $expout .= " <fraction>{$answer->fraction}</fraction>\n";
599 $expout .= "</answer>\n";
600 }
601
602 $units = $question->options->units;
603 if (count($units)) {
604 $expout .= "<units>\n";
605 foreach ($units as $unit) {
606 $expout .= " <unit>\n";
607 $expout .= " <multiplier>{$unit->multiplier}</multiplier>\n";
608 $expout .= " <unit_name>{$unit->unit}</unit_name>\n";
609 $expout .= " </unit>\n";
610 }
611 $expout .= "</units>\n";
612 }
613 break;
614 case MATCH:
615 foreach($question->options->subquestions as $subquestion) {
616 $expout .= "<subquestion>\n";
617 $expout .= $this->writetext( $subquestion->questiontext );
618 $expout .= "<answer>".$this->writetext( $subquestion->answertext )."</answer>\n";
619 $expout .= "</subquestion>\n";
620 }
621 break;
622 case DESCRIPTION:
623 // nothing more to do for this type
624 break;
625 case MULTIANSWER:
626 $expout .= "<!-- CLOZE type is not supported -->\n";
627 break;
628 default:
629 $expout .= "<!-- Question type is unknown or not supported (Type=$question->qtype) -->\n";
630 }
631
632 // close the question tag
633 $expout .= "</question>\n";
634
635 // run through xml tidy function
636 // $tidy_expout = $this->indent_xhtml( $expout, ' ' ) . "\n\n";
637
638 return $expout;
639 }
640}
641
642?>