PageRenderTime 60ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 0ms

/attempt/hp/renderer.php

https://github.com/KieranRBriggs/moodle-mod_hotpot
PHP | 923 lines | 553 code | 113 blank | 257 comment | 71 complexity | 107e4cbf9452655cf0291e19c595b861 MD5 | raw file
  1. <?php
  2. // This file is part of Moodle - http://moodle.org/
  3. //
  4. // Moodle is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // Moodle is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU General Public License
  15. // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
  16. /**
  17. * Render an attempt at a HotPot quiz
  18. * Output format: hp
  19. *
  20. * @package mod-hotpot
  21. * @copyright 2010 Gordon Bateson <gordon.bateson@gmail.com>
  22. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23. */
  24. defined('MOODLE_INTERNAL') || die();
  25. // get parent class
  26. require_once($CFG->dirroot.'/mod/hotpot/attempt/renderer.php');
  27. /**
  28. * mod_hotpot_attempt_hp_renderer
  29. *
  30. * @copyright 2010 Gordon Bateson
  31. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  32. * @since Moodle 2.0
  33. */
  34. class mod_hotpot_attempt_hp_renderer extends mod_hotpot_attempt_renderer {
  35. /** file name of main template e.g. jcloze6.ht_ */
  36. protected $templatefile = '';
  37. /** special template strings to be expanded */
  38. protected $templatestrings = '';
  39. /** templates folders (relative to Moodle dirroot) */
  40. protected $templatesfolders = array();
  41. /** external javascripts required for this format */
  42. protected $javascripts = array();
  43. /** type of javascript object to collect quiz results from browser */
  44. protected $js_object_type = '';
  45. // the id/name of the of the form which returns results to the browser
  46. protected $formid = 'store';
  47. // the name of the score field in the results returned form the browser
  48. protected $scorefield = 'mark';
  49. /* results form field that holds the start time of the attempt */
  50. protected $durationstartfield = 'starttime';
  51. /* results form field that holds the end time of the attempt */
  52. protected $durationfinishfield = 'endtime';
  53. // starttime/endtime are recorded by the client (and may not be trustworthy)
  54. // resumestart/resumefinish are recorded by the server (but include transfer time to and from client)
  55. /** the raw html content, either straight from an html file, or generated from an xml file */
  56. protected $htmlcontent;
  57. /** names of javascript string variables which can be passed through Glossary autolinking filter, if enabled */
  58. protected $headcontent_strings = '';
  59. /** names of javascript array variables which can be passed through Glossary autolinking filter, if enabled */
  60. protected $headcontent_arrays = '';
  61. // HP quizzes have a SubmissionTimeout variable, but TexToys do not
  62. protected $hasSubmissionTimeout = true;
  63. // basic initialization
  64. /**
  65. * init
  66. *
  67. * @param xxx $hotpot
  68. */
  69. public function init($hotpot) {
  70. parent::init($hotpot);
  71. array_push($this->javascripts, 'mod/hotpot/attempt/hp/hp.js');
  72. if ($hotpot->studentfeedback) {
  73. array_push($this->javascripts, 'mod/hotpot/attempt/hp/feedback.js');
  74. }
  75. }
  76. /**
  77. * set_xmldeclaration
  78. */
  79. function set_xmldeclaration() {
  80. // for IE6 we must *not* send an xmldeclaration; for other browsers we can
  81. // see http://moodle.org/mod/forum/discuss.php?d=73309
  82. if (! isset($this->xmldeclaration)) {
  83. if (isset($_SERVER['HTTP_USER_AGENT']) && strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE 6')) {
  84. // do not set xml declaration for IE6 otherwise it goes into quirks mode
  85. // TODO: check behavior if ($this->page->theme->name()=='custom_corners')
  86. $this->xmldeclaration = '';
  87. } else {
  88. $this->xmldeclaration = '<'.'?xml version="1.0"?'.'>'."\n";
  89. }
  90. }
  91. }
  92. /**
  93. * set_htmlcontent
  94. *
  95. * @return xxx
  96. */
  97. function set_htmlcontent() {
  98. if (isset($this->htmlcontent)) {
  99. // htmlcontent has already been set
  100. return true;
  101. }
  102. $this->htmlcontent = '';
  103. if (! $this->hotpot->get_source()) {
  104. // could not access source file
  105. return false;
  106. }
  107. if (! $this->hotpot->source->get_filecontents()) {
  108. // empty source file - shouldn't happen !!
  109. return false;
  110. }
  111. // html files
  112. if ($this->hotpot->source->is_html()) {
  113. $this->htmlcontent = &$this->hotpot->source->filecontents;
  114. return true;
  115. }
  116. // xml files
  117. if (! $this->hotpot->source->xml_get_filecontents()) {
  118. // could not create xml tree - shouldn't happen !!
  119. return false;
  120. }
  121. if (! $this->xml_set_htmlcontent()) {
  122. // could not create html from xml - shouldn't happen !!
  123. return false;
  124. }
  125. return true;
  126. }
  127. /**
  128. * xml_set_htmlcontent
  129. *
  130. * @return xxx
  131. */
  132. function xml_set_htmlcontent() {
  133. // get the xml source
  134. if (! $this->hotpot->source->xml_get_filecontents()) {
  135. // could not detect Hot Potatoes quiz type in xml file - shouldn't happen !!
  136. return false;
  137. }
  138. if (empty($this->templatefile)) {
  139. throw new moodle_exception('missingtemplatefile', 'hotpot', '', get_class($this));
  140. }
  141. if (! $this->htmlcontent = $this->expand_template($this->templatefile)) {
  142. // some problem accessing the main template for this quiz type
  143. return false;
  144. }
  145. if ($this->templatestrings) {
  146. $this->expand_strings($this->htmlcontent, '/\[('.$this->templatestrings.')\]/is');
  147. }
  148. // all done
  149. return true;
  150. }
  151. /**
  152. * set_headcontent
  153. */
  154. function set_headcontent() {
  155. global $CFG, $QUIZPORT;
  156. if (isset($this->headcontent)) {
  157. return;
  158. }
  159. $this->headcontent = '';
  160. if (! $this->set_htmlcontent()) {
  161. // could not locate/generate html content
  162. return;
  163. }
  164. // extract contents of first <head> tag
  165. if (preg_match($this->tagpattern('head'), $this->htmlcontent, $matches)) {
  166. $this->headcontent = $matches[2];
  167. }
  168. if ($this->usemoodletheme) {
  169. // remove the title from the <head>
  170. $this->headcontent = preg_replace($this->tagpattern('title'), '', $this->headcontent);
  171. } else {
  172. // replace <title> with current name of this quiz
  173. $title = '<title>'.$this->get_title().'</title>'."\n";
  174. $this->headcontent = preg_replace($this->tagpattern('title'), $title, $this->headcontent);
  175. // extract details needed to rebuild page later in $this->view()
  176. if (preg_match($this->tagpattern('\?xml','',false), $this->htmlcontent, $matches)) {
  177. $this->xmldeclaration = $matches[0]."\n";
  178. }
  179. if (preg_match($this->tagpattern('!DOCTYPE','',false), $this->htmlcontent, $matches)) {
  180. $this->doctype = $this->single_line($matches[0])."\n";
  181. // convert short dtd to full dtd (short dtd not allowed in xhtml 1.1)
  182. $this->doctype = preg_replace('/"xhtml(\d+)(-\w+)?.dtd"/i', '"http://www.w3.org/TR/xhtml$1/DTD/xhtml$1$2.dtd"', $this->doctype, 1);
  183. }
  184. if (preg_match($this->tagpattern('html','',false), $this->htmlcontent, $matches)) {
  185. $this->htmlattributes = ' '.$this->single_line($matches[1])."\n";
  186. }
  187. if (preg_match($this->tagpattern('head','',false), $this->htmlcontent, $matches)) {
  188. $this->headattributes = ' '.$this->single_line($matches[1]);
  189. }
  190. }
  191. // transfer <styles> tags from $this->headcontent to $this->styles
  192. $this->styles = '';
  193. if (preg_match_all($this->tagpattern('style'), $this->headcontent, $matches, PREG_OFFSET_CAPTURE)) {
  194. foreach (array_reverse($matches[0]) as $match) {
  195. // $match: [0] = matched string, [1] = offset to start of string
  196. $this->styles = $match[0]."\n".$this->styles;
  197. $this->headcontent = substr_replace($this->headcontent, '', $match[1], strlen($match[0]));
  198. }
  199. // restrict scope of Hot Potatoes styles, so they affect only the quiz's containing element (i.e. the middle column)
  200. if ($this->usemoodletheme) {
  201. $search = '/([a-z0-9_\#\.\-\,\: ]+){(.*?)}/is';
  202. $callback = array($this, 'fix_css_definitions');
  203. $this->styles = preg_replace_callback($search, $callback, $this->styles);
  204. // the following is not necessary for standard HP styles, but may required to handle some custom styles
  205. $this->styles = str_replace('TheBody', $this->themecontainer, $this->styles);
  206. }
  207. $this->styles = $this->remove_blank_lines($this->styles);
  208. }
  209. // transfer <script> tags from $this->headcontent to $this->scripts
  210. $this->scripts = '';
  211. if (preg_match_all($this->tagpattern('script'), $this->headcontent, $matches, PREG_OFFSET_CAPTURE)) {
  212. foreach (array_reverse($matches[0]) as $match) {
  213. // $match: [0] = matched string, [1] = offset to start of string
  214. $this->scripts = $match[0]."\n".$this->scripts;
  215. $this->headcontent = substr_replace($this->headcontent, '', $match[1], strlen($match[0]));
  216. }
  217. if ($this->usemoodletheme) {
  218. $this->scripts = str_replace('TheBody', $this->themecontainer, $this->scripts);
  219. }
  220. // fix various javascript functions
  221. $names = $this->get_js_functionnames();
  222. $this->fix_js_functions($names);
  223. $this->scripts = preg_replace('/\s*'.'(var )?ResultForm [^;]+?;/s', '', $this->scripts);
  224. // remove multi-line and single-line comments - except <![CDATA[ + ]]> and <!-- + -->
  225. if ($CFG->debug <= DEBUG_DEVELOPER) {
  226. $this->scripts = $this->fix_js_comment($this->scripts);
  227. }
  228. $this->scripts = $this->remove_blank_lines($this->scripts);
  229. // standardize "} else {" and "} else if" formatting
  230. $this->scripts = preg_replace('/\}\s*else\s*(\{|if)/s', '} else $1', $this->scripts);
  231. // standardize indentation to use tabs
  232. $this->scripts = str_replace(' ', "\t", $this->scripts);
  233. }
  234. // remove blank lines
  235. $this->headcontent = $this->remove_blank_lines($this->headcontent);
  236. // put each <meta> tag on its own line
  237. $this->headcontent = preg_replace('/'.'([^\n])'.'(<\w+)'.'/', '$1'."\n".'$2', $this->headcontent);
  238. // convert self closing tags to explictly closed tags (self-closing not allowed in xhtml 1.1)
  239. // $this->headcontent = preg_replace('/<((\w+)[^>]*?)\s*\/>/i', '<$1></$2>', $this->headcontent);
  240. // append styles and scripts to the end of the $this->headcontent
  241. $this->headcontent .= $this->styles.$this->scripts;
  242. // do any other fixes for the headcontent
  243. $this->fix_headcontent_beforeonunload();
  244. $this->fix_headcontent();
  245. }
  246. /**
  247. * fix_headcontent
  248. */
  249. function fix_headcontent() {
  250. // this function is a hook that can be used by sub classes to fix up the headcontent
  251. }
  252. /**
  253. * fix_headcontent_beforeonunload
  254. *
  255. * @return xxx
  256. */
  257. function fix_headcontent_beforeonunload() {
  258. global $QUIZPORT;
  259. // warn user about consequences of navigating away from this page
  260. switch ($this->can_continue()) {
  261. case hotpot::CONTINUE_RESUMEQUIZ:
  262. $onbeforeunload = get_string('canresumequiz', 'hotpot', format_string($QUIZPORT->quiz->name));
  263. break;
  264. case hotpot::CONTINUE_RESTARTQUIZ:
  265. $onbeforeunload = get_string('canrestartquiz', 'hotpot', format_string($QUIZPORT->quiz->name));
  266. break;
  267. case hotpot::CONTINUE_RESTARTUNIT:
  268. $onbeforeunload = get_string('canrestartunit', 'hotpot');
  269. break;
  270. case hotpot::CONTINUE_ABANDONUNIT:
  271. $onbeforeunload = get_string('abandonunit', 'hotpot');
  272. break;
  273. default:
  274. $onbeforeunload = ''; // shouldn't happen !!
  275. }
  276. if ($onbeforeunload) {
  277. $search = "/(\s*)window.onunload = new Function[^\r\n]*;/s";
  278. $replace = '$0$1'
  279. ."window.hotpotbeforeunload = function(){".'$1'
  280. ." return '".$this->hotpot->source->js_value_safe($onbeforeunload, true)."';".'$1'
  281. ."}".'$1'
  282. ."if (window.opera) {".'$1'
  283. // user scripts (this is here for reference only)
  284. // ." opera.setOverrideHistoryNavigationMode('compatible');".'$1'
  285. // web page scripts
  286. ." history.navigationMode = 'compatible';".'$1'
  287. ."}".'$1'
  288. ."window.onbeforeunload = window.hotpotbeforeunload;"
  289. ;
  290. $this->headcontent = preg_replace($search, $replace, $this->headcontent, 1);
  291. }
  292. }
  293. /**
  294. * fix_headcontent_rottmeier
  295. *
  296. * @param xxx $type (optional, default='')
  297. */
  298. function fix_headcontent_rottmeier($type='') {
  299. }
  300. /**
  301. * fix_js_comment
  302. *
  303. * @param xxx $str
  304. * @return xxx
  305. */
  306. function fix_js_comment($str) {
  307. $out = '';
  308. // the parse state
  309. // 0 : javascript code
  310. // 1 : single-quoted string
  311. // 2 : double-quoted string
  312. // 3 : single line comment
  313. // 4 : multi-line comment
  314. $state = 0;
  315. $strlen = strlen($str);
  316. $i = 0;
  317. while ($i<$strlen) {
  318. switch ($state) {
  319. case 1: // single-quoted string
  320. $out .= $str{$i};
  321. switch ($str{$i}) {
  322. case "\\":
  323. $i++; // skip next char
  324. $out .= $str{$i};
  325. break;
  326. case "\n":
  327. case "\r":
  328. case "'":
  329. $state = 0; // end of this string
  330. break;
  331. }
  332. break;
  333. case 2: // double-quoted string
  334. $out .= $str{$i};
  335. switch ($str{$i}) {
  336. case "\\":
  337. $i++; // skip next char
  338. $out .= $str{$i};
  339. break;
  340. case "\n":
  341. case "\r":
  342. case '"':
  343. $state = 0; // end of this string
  344. break;
  345. }
  346. break;
  347. case 3: // single line comment
  348. if ($str{$i}=="\n" || $str{$i}=="\r") {
  349. $state = 0; // end of this comment
  350. $out .= $str{$i};
  351. }
  352. break;
  353. case 4: // multi-line comment
  354. if ($str{$i}=='*' && $str{$i+1}=='/') {
  355. $state = 0; // end of this comment
  356. $i++;
  357. }
  358. break;
  359. case 0: // plain old JavaScript code
  360. default:
  361. switch ($str{$i}) {
  362. case "'":
  363. $out .= $str{$i};
  364. $state = 1; // start of single quoted string
  365. break;
  366. case '"':
  367. $out .= $str{$i};
  368. $state = 2; // start of double quoted string
  369. break;
  370. case '/':
  371. switch ($str{$i+1}) {
  372. case '/':
  373. switch (true) {
  374. // allow certain single line comments
  375. case substr($str, $i+2, 9)=='<![CDATA[':
  376. $out .= substr($str, $i, 11);
  377. $i += 11;
  378. break;
  379. case substr($str, $i+2, 3)==']]>':
  380. $out .= substr($str, $i, 5);
  381. $i += 5;
  382. break;
  383. case substr($str, $i+2, 4)=='<!--':
  384. $out .= substr($str, $i, 6);
  385. $i += 6;
  386. break;
  387. case substr($str, $i+2, 3)=='-->':
  388. $out .= substr($str, $i, 5);
  389. $i += 5;
  390. break;
  391. default:
  392. $state = 3; // start of single line comment
  393. $i++;
  394. break;
  395. }
  396. break;
  397. case '*':
  398. $state = 4; // start of multi-line comment
  399. $i++;
  400. break;
  401. default:
  402. // a slash - could be start of RegExp ?!
  403. $out .= $str{$i};
  404. }
  405. break;
  406. default:
  407. $out .= $str{$i};
  408. } // end switch : non-comment char
  409. } // end switch : status
  410. $i++;
  411. } // end while
  412. return $out;
  413. }
  414. /**
  415. * set_bodycontent
  416. */
  417. function set_bodycontent() {
  418. if (isset($this->bodycontent)) {
  419. // content was fetched from cache
  420. return;
  421. }
  422. // otherwise we need to generate new body content
  423. $this->bodycontent = '';
  424. if (! $this->set_htmlcontent()) {
  425. // could not locate/generate html content
  426. return;
  427. }
  428. // extract <body> tag
  429. if (! preg_match($this->tagpattern('body', 'onload'), $this->htmlcontent, $matches)) {
  430. return false;
  431. }
  432. if ($this->usemoodletheme) {
  433. $matches[1] = str_replace('id="TheBody"', '', $matches[1]);
  434. }
  435. $this->bodyattributes = $this->single_line($matches[1]);
  436. $onload = $matches[3];
  437. $this->bodycontent = $this->remove_blank_lines($matches[5]);
  438. // where necessary, add single space before javascript event handlers to make the syntax compatible with strict XHTML
  439. $this->bodycontent = preg_replace('/"(on(?:blur|click|focus|mouse(?:down|out|over|up)))/', '" $1', $this->bodycontent);
  440. // ensure javascript onload routine for quiz is always executed
  441. // $this->bodyattributes will only be inserted into the <body ...> tag
  442. // if it is included in the theme/$CFG->theme/header.html,
  443. // so some old or modified themes may not insert $this->bodyattributes
  444. $this->bodycontent .= $this->fix_onload($onload, true);
  445. $this->fix_title();
  446. $this->fix_TimeLimit();
  447. $this->fix_SubmissionTimeout();
  448. $this->fix_relativeurls();
  449. $this->fix_navigation();
  450. $this->fix_filters();
  451. $this->fix_mediafilter();
  452. $this->fix_feedbackform();
  453. $this->fix_reviewoptions();
  454. $this->fix_targets();
  455. $this->fix_bodycontent();
  456. }
  457. /**
  458. * fix_bodycontent
  459. */
  460. function fix_bodycontent() {
  461. // this function is a hook that can be used by sub classes to fix up the bodycontent
  462. }
  463. /**
  464. * fix_bodycontent_rottmeier
  465. *
  466. * @param xxx $hideclozeform (optional, default=false)
  467. */
  468. function fix_bodycontent_rottmeier($hideclozeform=false) {
  469. }
  470. /**
  471. * fix_js_functions
  472. *
  473. * @param xxx $names
  474. */
  475. function fix_js_functions($names) {
  476. if (is_string($names)) {
  477. $names = explode(',', $names);
  478. }
  479. foreach($names as $name) {
  480. list($start, $finish) = $this->locate_js_function($name, $this->scripts);
  481. if (! $finish) {
  482. // debugging("Could not locate JavaScript function: $name", DEBUG_DEVELOPER);
  483. continue;
  484. }
  485. $methodname = "fix_js_{$name}";
  486. if (! method_exists($this, $methodname)) {
  487. // debugging("Could not locate method to fix JavaScript function: $name", DEBUG_DEVELOPER);
  488. continue;
  489. }
  490. $this->$methodname($this->scripts, $start, ($finish - $start));
  491. }
  492. }
  493. /**
  494. * locate_js_function
  495. *
  496. * @param xxx $name
  497. * @param xxx $str (passed by reference)
  498. * @param xxx $includewhitespace (optional, default=false)
  499. * @return xxx
  500. */
  501. function locate_js_function($name, &$str, $includewhitespace=false) {
  502. $start = 0;
  503. $finish = 0;
  504. if ($includewhitespace) {
  505. $search = '/\s*'.'function '.$name.'\b/s';
  506. } else {
  507. $search = '/\b'.'function '.$name.'\b/';
  508. }
  509. if (preg_match($search, $str, $matches, PREG_OFFSET_CAPTURE)) {
  510. // $matches[0][0] : matching string
  511. // $matches[0][1] : offset to matching string
  512. $start = $matches[0][1];
  513. // position of opening curly bracket (or thereabouts)
  514. $i = $start + strlen($matches[0][0]);
  515. // count how many opening curly brackets we have had so far
  516. $count = 0;
  517. // the parse state
  518. // 0 : javascript code
  519. // 1 : single-quoted string
  520. // 2 : double-quoted string
  521. // 3 : single line comment
  522. // 4 : multi-line comment
  523. $state = 0;
  524. $strlen = strlen($str);
  525. while ($i<$strlen && ! $finish) {
  526. switch ($state) {
  527. case 1: // single-quoted string
  528. switch ($str{$i}) {
  529. case "\\":
  530. $i++; // skip next char
  531. break;
  532. case "'":
  533. $state = 0; // end of this string
  534. break;
  535. }
  536. break;
  537. case 2: // double-quoted string
  538. switch ($str{$i}) {
  539. case "\\":
  540. $i++; // skip next char
  541. break;
  542. case '"':
  543. $state = 0; // end of this string
  544. break;
  545. }
  546. break;
  547. case 3: // single line comment
  548. if ($str{$i}=="\n" || $str{$i}=="\r") {
  549. $state = 0; // end of this comment
  550. }
  551. break;
  552. case 4: // multi-line comment
  553. if ($str{$i}=='*' && $str{$i+1}=='/') {
  554. $state = 0; // end of this comment
  555. $i++;
  556. }
  557. break;
  558. case 0: // plain old JavaScript code
  559. default:
  560. switch ($str{$i}) {
  561. case "'":
  562. $state = 1; // start of single quoted string
  563. break;
  564. case '"':
  565. $state = 2; // start of double quoted string
  566. break;
  567. case '/':
  568. switch ($str{$i+1}) {
  569. case '/':
  570. $state = 3; // start of single line comment
  571. $i++;
  572. break;
  573. case '*':
  574. $state = 4; // start of multi-line comment
  575. $i++;
  576. break;
  577. }
  578. break;
  579. case '{':
  580. $count++; // start of Javascript code block
  581. break;
  582. case '}':
  583. $count--; // end of Javascript code block
  584. if ($count==0) { // end of outer code block (i.e. end of function)
  585. $finish = $i + 1;
  586. }
  587. break;
  588. } // end switch : non-comment char
  589. } // end switch : status
  590. $i++;
  591. } // end while
  592. } // end if $start
  593. return array($start, $finish);
  594. }
  595. // does this output format allow quiz attempts to be reviewed?
  596. /**
  597. * provide_review
  598. *
  599. * @return xxx
  600. */
  601. public function provide_review() {
  602. return true;
  603. }
  604. // functions to expand xml templates (and the blocks and strings contained therein)
  605. /**
  606. * expand_template
  607. *
  608. * @param xxx $filename
  609. * @return xxx
  610. */
  611. public function expand_template($filename) {
  612. global $CFG;
  613. // check that some template folders have been specified to something sensible
  614. if (! isset($this->templatesfolders)) {
  615. debugging('templatesfolders is not set', DEBUG_DEVELOPER);
  616. return '';
  617. }
  618. if (! is_array($this->templatesfolders)) {
  619. debugging('templatesfolders is not an array', DEBUG_DEVELOPER);
  620. return '';
  621. }
  622. // set the path to the template file
  623. $filepath = '';
  624. foreach ($this->templatesfolders as $templatesfolder) {
  625. if (is_file("$CFG->dirroot/$templatesfolder/$filename")) {
  626. $filepath = "$CFG->dirroot/$templatesfolder/$filename";
  627. break;
  628. }
  629. }
  630. // check the template was found
  631. if (! $filepath) {
  632. debugging('template not found: '.$filename, DEBUG_DEVELOPER);
  633. return '';
  634. }
  635. // check the template is readable
  636. if (! is_readable($filepath)) {
  637. debugging('template is not readable: '.$filepath, DEBUG_DEVELOPER);
  638. return '';
  639. }
  640. // try and read the template
  641. if (! $template = file_get_contents($filepath)) {
  642. debugging('template is empty: '.$filepath, DEBUG_DEVELOPER);
  643. return '';
  644. }
  645. // expand the blocks and strings in the template
  646. $this->expand_blocks($template);
  647. $this->expand_strings($template);
  648. // return the expanded template
  649. return $template;
  650. }
  651. /**
  652. * expand_blocks
  653. *
  654. * @param xxx $template (passed by reference)
  655. */
  656. public function expand_blocks(&$template) {
  657. // expand conditional blocks
  658. // [1] the full block name (including optional leading 'str' or 'incl')
  659. // [2] the short block name (without optional leading 'str' or 'incl')
  660. // [3] the content of the block
  661. $search = '/'.'\[((?:incl|str)?((?:\w|\.)+))\]'.'(.*?)'.'\[\/\\1\]'.'/is';
  662. $callback = array($this, 'expand_block');
  663. $template = preg_replace_callback($search, $callback, $template);
  664. }
  665. /**
  666. * expand_block
  667. *
  668. * @param xxx $match
  669. * @return xxx
  670. */
  671. public function expand_block($match) {
  672. $blockname = $match[2];
  673. $blockcontent = $match[3];
  674. // check expand method exists
  675. $method = 'expand_'.str_replace('.', '', $blockname);
  676. if (! method_exists($this, $method)) {
  677. debugging('expand block method not found: '.$method, DEBUG_DEVELOPER);
  678. return '';
  679. }
  680. // if condition is satisfied, return block content; otherwise return empty string
  681. if ($this->$method()) {
  682. // expand any (sub) blocks within the block content
  683. $this->expand_blocks($blockcontent);
  684. return $blockcontent;
  685. } else {
  686. return '';
  687. }
  688. }
  689. /**
  690. * expand_strings
  691. *
  692. * @param xxx $template (passed by reference)
  693. * @param xxx $search (optional, default='')
  694. */
  695. public function expand_strings(&$template, $search='') {
  696. if ($search=='') {
  697. // default search string
  698. $search = '/\[(?:bool|int|str)(\w+)\]/is';
  699. }
  700. $callback = array($this, 'expand_string');
  701. $template = preg_replace_callback($search, $callback, $template);
  702. }
  703. /**
  704. * expand_string
  705. *
  706. * @param xxx $match
  707. * @return xxx
  708. */
  709. public function expand_string($match) {
  710. $originalstring = $match[0];
  711. $stringname = $match[1];
  712. $method = 'expand_'.$stringname;
  713. if (method_exists($this, $method)) {
  714. return $this->$method();
  715. } else {
  716. return $originalstring;
  717. }
  718. }
  719. /**
  720. * expand_halfway_color
  721. *
  722. * @param xxx $x
  723. * @param xxx $y
  724. * @return xxx
  725. */
  726. public function expand_halfway_color($x, $y) {
  727. // returns the $color that is half way between $x and $y
  728. $color = $x; // default
  729. $rgb = '/^\#?([0-9a-f])([0-9a-f])([0-9a-f])$/i';
  730. $rrggbb = '/^\#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i';
  731. if (preg_match($rgb, $x, $x_matches) || preg_match($rrggbb, $x, $x_matches)) {
  732. if (preg_match($rgb, $y, $y_matches) || preg_match($rrggbb, $y, $y_matches)) {
  733. $color = '#';
  734. for ($i=1; $i<=3; $i++) {
  735. $x_dec = hexdec($x_matches[$i]);
  736. $y_dec = hexdec($y_matches[$i]);
  737. $color .= sprintf('%02x', min($x_dec, $y_dec) + abs($x_dec-$y_dec)/2);
  738. }
  739. }
  740. }
  741. return $color;
  742. }
  743. // functions to convert relative urls to absolute URLs
  744. /**
  745. * fix_relativeurls
  746. *
  747. * @param xxx $str (optional, default=null)
  748. * @return xxx
  749. */
  750. public function fix_relativeurls($str=null) {
  751. global $DB;
  752. if (is_string($str)) {
  753. // fix relative urls in $str(ing), and return
  754. return parent::fix_relativeurls($str);
  755. }
  756. // do standard fixes relative urls in $this->headcontent and $this->bodycontent
  757. parent::fix_relativeurls();
  758. // replace relative URLs in "PreloadImages(...);"
  759. $search = '/(PreloadImages\()'.'([^)]+?)'.'(\);)/is';
  760. $callback = array($this, 'convert_urls_preloadimages');
  761. $this->headcontent = preg_replace_callback($search, $callback, $this->headcontent);
  762. $this->bodycontent = preg_replace_callback($search, $callback, $this->bodycontent);
  763. }
  764. /**
  765. * convert_urls_preloadimages
  766. *
  767. * @param xxx $match
  768. * @return xxx
  769. */
  770. public function convert_urls_preloadimages($match) {
  771. $before = $match[1];
  772. $urls = $match[2];
  773. $after = $match[3];
  774. $search = '/('.'"'.'|'."'".')'."([^'".'",]+?)'.'('.'"'.'|'."'".')/is';
  775. $callback = array($this, 'convert_url');
  776. return $before.preg_replace_callback($search, $callback, $urls).$after;
  777. }
  778. /**
  779. * convert_url_navbutton
  780. *
  781. * @param xxx $match
  782. * @return xxx
  783. */
  784. public function convert_url_navbutton($match) {
  785. global $CFG, $DB;
  786. $url = $this->convert_url($match[1]);
  787. // is this a $url for another HotPot in this course ?
  788. if (strpos($url, $this->hotpot->source->baseurl.'/')===0) {
  789. $filepath = substr($url, strlen($this->hotpot->source->baseurl));
  790. $sourcefile = $this->hotpot->source->xml_locate_file($filepath);
  791. if ($records = $DB->get_records('hotpot', array('sourcefile' => $sourcefile), '', 'id', 0, 1)) {
  792. $record = reset($records); // first record - there could be more than one ?!
  793. $url = new moodle_url('/mod/hotpot/view.php', array('id' => $record->id));
  794. }
  795. }
  796. return $url;
  797. }
  798. }