PageRenderTime 68ms CodeModel.GetById 34ms RepoModel.GetById 4ms app.codeStats 0ms

/source/hp/class.php

https://github.com/KieranRBriggs/moodle-mod_hotpot
PHP | 675 lines | 398 code | 64 blank | 213 comment | 71 complexity | 6f0e87678f654484fb97ef8c6f7ecfe4 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. * Class to represent the source of a HotPot quiz
  18. * Source type: 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/source/class.php');
  27. /**
  28. * hotpot_source_hp
  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 hotpot_source_hp extends hotpot_source {
  35. public $xml; // an array containing the xml tree for hp xml files
  36. public $xml_root; // the array key of the root of the xml tree
  37. public $hbs_software; // hotpot or textoys
  38. public $hbs_quiztype; // jcloze, jcross, jmatch, jmix, jquiz, quandary, rhubarb, sequitur
  39. /**
  40. * is_html
  41. *
  42. * @return xxx
  43. */
  44. function is_html() {
  45. return preg_match('/\.html?$/', $this->file->get_filename());
  46. }
  47. /**
  48. * get_name
  49. *
  50. * @return xxx
  51. */
  52. function get_name() {
  53. if ($this->is_html()) {
  54. return $this->html_get_name();
  55. } else {
  56. return $this->xml_get_name();
  57. }
  58. }
  59. /**
  60. * get_title
  61. *
  62. * @return xxx
  63. */
  64. function get_title() {
  65. if ($this->is_html()) {
  66. return $this->html_get_name(false);
  67. } else {
  68. return $this->xml_get_name(false);
  69. }
  70. }
  71. /**
  72. * get_entrytext
  73. *
  74. * @return xxx
  75. */
  76. function get_entrytext() {
  77. if ($this->is_html()) {
  78. return $this->html_get_entrytext();
  79. } else {
  80. return $this->xml_get_entrytext();
  81. }
  82. }
  83. /**
  84. * get_nextquiz
  85. *
  86. * @return xxx
  87. */
  88. function get_nextquiz() {
  89. if ($this->is_html()) {
  90. return $this->html_get_nextquiz();
  91. } else {
  92. return $this->xml_get_nextquiz();
  93. }
  94. }
  95. // function for html files
  96. /**
  97. * html_get_name
  98. *
  99. * @param xxx $textonly (optional, default=true)
  100. * @return xxx
  101. */
  102. function html_get_name($textonly=true) {
  103. if (! isset($this->name)) {
  104. $this->name = '';
  105. $this->title = '';
  106. if (! $this->get_filecontents()) {
  107. // empty file - shouldn't happen !!
  108. return false;
  109. }
  110. if (preg_match('/<h2[^>]*class="ExerciseTitle"[^>]*>(.*?)<\/h2>/is', $this->filecontents, $matches)) {
  111. $this->name = trim(strip_tags($matches[1]));
  112. $this->title = trim($matches[1]);
  113. }
  114. if (! $this->name) {
  115. if (preg_match('/<title[^>]*>(.*?)<\/title>/is', $this->filecontents, $matches)) {
  116. $this->name = trim(strip_tags($matches[1]));
  117. if (! $this->title) {
  118. $this->title = trim($matches[1]);
  119. }
  120. }
  121. }
  122. $this->name = hotpot_textlib('entities_to_utf8', $this->name, true);
  123. $this->title = hotpot_textlib('entities_to_utf8', $this->title, true);
  124. }
  125. if ($textonly) {
  126. return $this->name;
  127. } else {
  128. return $this->title;
  129. }
  130. }
  131. /**
  132. * html_get_entrytext
  133. *
  134. * @return xxx
  135. */
  136. function html_get_entrytext() {
  137. if (! isset($this->entrytext)) {
  138. $this->entrytext = '';
  139. if (! $this->get_filecontents()) {
  140. // empty file - shouldn't happen !!
  141. return false;
  142. }
  143. if (preg_match('/<h3[^>]*class="ExerciseSubtitle"[^>]*>\s*(.*?)\s*<\/h3>/is', $this->filecontents, $matches)) {
  144. $this->entrytext .= '<div>'.$matches[1].'</div>';
  145. }
  146. if (preg_match('/<div[^>]*id="Instructions"[^>]*>\s*(.*?)\s*<\/div>/is', $this->filecontents, $matches)) {
  147. $this->entrytext .= '<div>'.$matches[1].'</div>';
  148. }
  149. }
  150. return $this->entrytext;
  151. }
  152. /**
  153. * html_get_nextquiz
  154. *
  155. * @return xxx
  156. */
  157. function html_get_nextquiz() {
  158. if (! isset($this->nextquiz)) {
  159. $this->nextquiz = false;
  160. if (! $this->get_filecontents()) {
  161. // empty file - shouldn't happen !!
  162. return false;
  163. }
  164. if (preg_match('/<div[^>]*class="NavButtonBar"[^>]*>(.*?)<\/div>/is', $this->filecontents, $matches)) {
  165. $navbuttonbar = $matches[1];
  166. if (preg_match_all('/<button[^>]*onclick="'."location='([^']*)'".'[^"]*"[^>]*>/is', $navbuttonbar, $matches)) {
  167. $lastbutton = count($matches[0])-1;
  168. $this->nextquiz = $this->xml_locate_file(dirname($this->filepath).'/'.$matches[1][$lastbutton]);
  169. }
  170. }
  171. }
  172. return $this->nextquiz;
  173. }
  174. // functions for xml files
  175. /**
  176. * xml_get_name
  177. *
  178. * @param xxx $textonly (optional, default=true)
  179. * @return xxx
  180. */
  181. function xml_get_name($textonly=true) {
  182. if (! isset($this->name)) {
  183. $this->name = '';
  184. $this->title = '';
  185. if (! $this->xml_get_filecontents()) {
  186. // could not detect Hot Potatoes quiz type - shouldn't happen !!
  187. return false;
  188. }
  189. $this->title = $this->xml_value('data,title');
  190. $this->title = hotpot_textlib('entities_to_utf8', $this->title, true);
  191. $this->name = trim(strip_tags($this->title)); // sanitize
  192. }
  193. if ($textonly) {
  194. return $this->name;
  195. } else {
  196. return $this->title;
  197. }
  198. }
  199. /**
  200. * xml_get_entrytext
  201. *
  202. * @return xxx
  203. */
  204. function xml_get_entrytext() {
  205. if (! isset($this->entrytext)) {
  206. $this->entrytext = '';
  207. if (! $this->xml_get_filecontents()) {
  208. // could not detect Hot Potatoes quiz type - shouldn't happen !!
  209. return false;
  210. }
  211. if ($intro = $this->xml_value($this->hbs_software.'-config-file,'.$this->hbs_quiztype.',exercise-subtitle')) {
  212. $this->entrytext .= '<h3>'.$intro.'</h3>';
  213. }
  214. if ($intro = $this->xml_value($this->hbs_software.'-config-file,'.$this->hbs_quiztype.',instructions')) {
  215. $this->entrytext .= '<div>'.$intro.'</div>';
  216. }
  217. }
  218. return $this->entrytext;
  219. }
  220. /**
  221. * xml_get_nextquiz
  222. *
  223. * @return xxx
  224. */
  225. function xml_get_nextquiz() {
  226. if (! isset($this->nextquiz)) {
  227. $this->nextquiz = false;
  228. if (! $this->xml_get_filecontents()) {
  229. // could not detect Hot Potatoes quiz type in xml file - shouldn't happen !!
  230. return false;
  231. }
  232. if (! $this->xml_value_int($this->hbs_software.'-config-file,global,include-next-ex')) {
  233. // next exercise is not enabled for this quiz
  234. return false;
  235. }
  236. if (! $nextquiz = $this->xml_value($this->hbs_software.'-config-file,'.$this->hbs_quiztype.',next-ex-url')) {
  237. // there is no next URL given for the next quiz
  238. return false;
  239. }
  240. // set the URL of the next quiz
  241. $this->nextquiz = $this->xml_locate_file(dirname($this->filepath).'/'.$nextquiz);
  242. }
  243. return $this->nextquiz;
  244. }
  245. /**
  246. * xml_locate_file
  247. *
  248. * @param xxx $file
  249. * @param xxx $filetypes (optional, default=null)
  250. * @return xxx
  251. */
  252. function xml_locate_file($file, $filetypes=null) {
  253. if (preg_match('/^https?:\/\//', $file)) {
  254. return $file;
  255. }
  256. $filepath = $this->basepath.'/'.ltrim($file, '/');
  257. if (file_exists($filepath)) {
  258. return $file;
  259. }
  260. $filename = basename($filepath);
  261. if (! $pos = strrpos($filename, '.')) {
  262. return $file;
  263. }
  264. $filetype = substr($filename, $pos + 1);
  265. if ($filetype=='htm' || $filetype=='html') {
  266. // $file is a local html file that doesn't exist
  267. // so search for a HP source file with the same name
  268. $len = strlen($filetype);
  269. $filepath = substr($filepath, 0, -$len);
  270. if (is_null($filetypes)) {
  271. $filetypes = array('jcl', 'jcw', 'jmt', 'jmx', 'jqz'); // 'jbc' for HP 5 ?
  272. }
  273. foreach ($filetypes as $filetype) {
  274. if (file_exists($filepath.$filetype)) {
  275. return substr($file, 0, -$len).$filetype;
  276. }
  277. }
  278. }
  279. // valid $file could not be found :-(
  280. return '';
  281. }
  282. /**
  283. * xml_get_filecontents
  284. *
  285. * @return xxx
  286. */
  287. function xml_get_filecontents() {
  288. if (! isset($this->xml)) {
  289. $this->xml = false;
  290. $this->xml_root = '';
  291. if (! $this->get_filecontents()) {
  292. // empty file - shouldn't happen !!
  293. return false;
  294. }
  295. $this->compact_filecontents();
  296. $this->pre_xmlize_filecontents();
  297. if (! $this->xml = xmlize($this->filecontents, 0)) {
  298. debugging('Could not parse XML file: '.$this->filepath);
  299. }
  300. $this->xml_root = $this->hbs_software.'-'.$this->hbs_quiztype.'-file';
  301. if (! array_key_exists($this->xml_root, $this->xml)) {
  302. debugging('Could not find XML root node: '.$this->xml_root);
  303. }
  304. if (isset($this->config) && $this->config->get_filecontents()) {
  305. $this->config->compact_filecontents();
  306. $xml = xmlize($this->config->filecontents, 0);
  307. $config_file = $this->hbs_software.'-config-file';
  308. if (isset($xml[$config_file]['#']) && isset($this->xml[$this->xml_root]['#'])) {
  309. // make sure the xml tree has the expected structure
  310. if (! isset($this->xml[$this->xml_root]['#'][$config_file][0]['#'])) {
  311. if (! isset($this->xml[$this->xml_root]['#'][$config_file][0])) {
  312. if (! isset($this->xml[$this->xml_root]['#'][$config_file])) {
  313. $this->xml[$this->xml_root]['#'][$config_file] = array();
  314. }
  315. $this->xml[$this->xml_root]['#'][$config_file][0] = array();
  316. }
  317. $this->xml[$this->xml_root]['#'][$config_file][0]['#'] = array();
  318. }
  319. // reference to the config values in $this->xml
  320. $config = &$this->xml[$this->xml_root]['#'][$config_file][0]['#'];
  321. $items = array_keys($xml[$config_file]['#']);
  322. foreach ($items as $item) { // 'global', 'jcloze', ... etc ..., 'version'
  323. if (is_array($xml[$config_file]['#'][$item][0]['#'])) {
  324. $values = array_keys($xml[$config_file]['#'][$item][0]['#']);
  325. foreach ($values as $value) {
  326. $config[$item][0]['#'][$value] = $xml[$config_file]['#'][$item][0]['#'][$value];
  327. }
  328. }
  329. }
  330. }
  331. }
  332. }
  333. return $this->xml ? true : false;
  334. }
  335. /**
  336. * pre_xmlize_filecontents
  337. */
  338. function pre_xmlize_filecontents() {
  339. if ($this->filecontents) {
  340. // encode all ampersands that are not part of HTML entities
  341. // http://stackoverflow.com/questions/310572/regex-in-php-to-match-that-arent-html-entities
  342. // Note: we could also use '<![CDATA[&]]>' as the replace string
  343. $search = '/&(?!(?:[a-zA-Z]+|#[0-9]+|#x[0-9a-fA-F]+);)/';
  344. $this->filecontents = preg_replace($search, '&amp;', $this->filecontents);
  345. //$this->filecontents = $hotpot_textlib('utf8_to_entities', $this->filecontents);
  346. // unfortunately textlib does not convert single-byte non-ascii chars
  347. // i.e. "Latin-1 Supplement" e.g. latin small letter with acute (&#237;)
  348. // unicode characters can be detected by checking the hex value of a character
  349. // 00 - 7F : ascii char (roman alphabet + punctuation)
  350. // 80 - BF : byte 2, 3 or 4 of a unicode char
  351. // C0 - DF : 1st byte of 2-byte char
  352. // E0 - EF : 1st byte of 3-byte char
  353. // F0 - FF : 1st byte of 4-byte char
  354. // if the string doesn't match the above, it might be
  355. // 80 - FF : single-byte, non-ascii char
  356. $search = '/'.'[\xc0-\xdf][\x80-\xbf]{1}'.'|'.
  357. '[\xe0-\xef][\x80-\xbf]{2}'.'|'.
  358. '[\xf0-\xff][\x80-\xbf]{3}'.'|'.
  359. '[\x80-\xff]'.'/';
  360. $callback = array($this, 'utf8_char_to_html_entity');
  361. $this->filecontents = preg_replace_callback($search, $callback, $this->filecontents);
  362. }
  363. }
  364. function utf8_char_to_html_entity($char, $ampersand='&') {
  365. // thanks to: http://www.zend.com/codex.php?id=835&single=1
  366. if (is_array($char)) {
  367. $char = $char[0];
  368. }
  369. // array used to figure what number to decrement from character order value
  370. // according to number of characters used to map unicode to ascii by utf-8
  371. static $HOTPOT_UTF8_DECREMENT = array(
  372. 1 => 0,
  373. 2 => 192,
  374. 3 => 224,
  375. 4 => 240
  376. );
  377. // the number of bits to shift each character by
  378. static $HOTPOT_UTF8_SHIFT = array(
  379. 1 => array(0=>0),
  380. 2 => array(0=>6, 1=>0),
  381. 3 => array(0=>12, 1=>6, 2=>0),
  382. 4 => array(0=>18, 1=>12, 2=>6, 3=>0)
  383. );
  384. $dec = 0;
  385. $len = strlen($char);
  386. for ($pos=0; $pos<$len; $pos++) {
  387. $ord = ord ($char{$pos});
  388. $ord -= ($pos ? 128 : $HOTPOT_UTF8_DECREMENT[$len]);
  389. $dec += ($ord << $HOTPOT_UTF8_SHIFT[$len][$pos]);
  390. }
  391. return $ampersand.'#x'.sprintf('%04X', $dec).';';
  392. }
  393. /**
  394. * xml_value
  395. *
  396. * @param xxx $tags
  397. * @param xxx $more_tags (optional, default=null)
  398. * @param xxx $default (optional, default='')
  399. * @return xxx
  400. */
  401. function xml_value($tags, $more_tags=null, $default='', $nl2br=true) {
  402. global $CFG;
  403. static $block_elements = null;
  404. // set reference to a $value in $this->xml array
  405. if (isset($this->xml_root)) {
  406. $all_tags = "['".$this->xml_root."']['#']";
  407. } else {
  408. $all_tags = ''; // shouldn't happen
  409. }
  410. if ($tags) {
  411. $all_tags .= "['".str_replace(",", "'][0]['#']['", $tags)."']";
  412. }
  413. if ($more_tags===null) {
  414. $all_tags .= "[0]['#']";
  415. } else {
  416. $all_tags .= $more_tags;
  417. }
  418. $all_tags = explode('][', str_replace("'", '', substr($all_tags, 1, -1)));
  419. $value = $this->xml;
  420. foreach ($all_tags as $tag) {
  421. if (! is_array($value)) {
  422. return null;
  423. }
  424. if(! array_key_exists($tag, $value)) {
  425. return null;
  426. }
  427. $value = $value[$tag];
  428. }
  429. if (is_string($value)) {
  430. // decode angle brackets
  431. $value = strtr($value, array('&#x003C;'=>'<', '&#x003E;'=>'>', '&#x0026;'=>'&'));
  432. // remove white space before and after HTML block elements
  433. if ($block_elements===null) {
  434. // set regexp to detect white space around html block elements
  435. $block_elements = array(
  436. //'div','p','pre','blockquote','center',
  437. //'h1','h2','h3','h4','h5','h6','hr',
  438. 'table','caption','colgroup','col','tbody','thead','tfoot','tr','th','td',
  439. 'ol','ul','dl','li','dt','dd',
  440. 'applet','embed','object','param',
  441. 'select','optgroup','option',
  442. 'fieldset','legend',
  443. 'frameset','frame'
  444. );
  445. $space = '(?:\s|(?:<br[^>]*>))*'; // unwanted white space
  446. $block_elements = '(?:\/?'.implode(')|(?:\/?', $block_elements).')';
  447. $block_elements = '/'.$space.'(<(?:'.$block_elements.')[^>]*>)'.$space.'/is';
  448. //.'(?='.'<)' // followed by the start of another tag
  449. }
  450. $value = preg_replace($block_elements, '$1', $value);
  451. // standardize whitespace within tags
  452. // $1 : start of tag i.e. "<"
  453. // $2 : chars in tag (including whitespace and <br />)
  454. // $3 : end of tag i.e. ">"
  455. $search = '/(<)([^>]*)(>)/is';
  456. $callback = array($this, 'single_line');
  457. $value = preg_replace_callback($search, $callback, $value);
  458. // replace remaining newlines with <br /> but not in <script> or <style> blocks
  459. // $1 : chars before open text
  460. // $2 : text to be converted
  461. // $3 : chars following text
  462. if ($nl2br) {
  463. $search = '/(^|(?:<\/(?:script|style)>\s?))(.*?)((?:\s?<(?:script|style)[^>]*>)|$)/is';
  464. $callback = array($this, 'xml_value_nl2br');
  465. $value = preg_replace_callback($search, $callback, $value);
  466. }
  467. }
  468. return $value;
  469. }
  470. /**
  471. * single_line
  472. *
  473. * @param xxx $match
  474. * @return xxx
  475. */
  476. function single_line($match) {
  477. if (is_string($match)) {
  478. $before = '';
  479. $text = $match;
  480. $after = '';
  481. } else {
  482. $before = $match[1];
  483. $text = $match[2];
  484. $after = $match[3];
  485. }
  486. return $before.preg_replace('/(?:(?:<br[^>]*>)|\s)+/is', ' ', $text).$after;
  487. }
  488. /**
  489. * xml_value_nl2br
  490. *
  491. * @param xxx $match
  492. * @return xxx
  493. */
  494. function xml_value_nl2br($match) {
  495. $before = $match[1];
  496. $text = $match[2];
  497. $after = $match[3];
  498. return $before.str_replace("\n", '<br />', $text).$after;
  499. }
  500. /**
  501. * xml_value_bool
  502. *
  503. * @param xxx $tags
  504. * @param xxx $more_tags (optional, default=null)
  505. * @param xxx $default (optional, default='')
  506. * @return xxx
  507. */
  508. function xml_value_bool($tags, $more_tags=null, $default=false) {
  509. $value = $this->xml_value($tags, $more_tags, $default, false);
  510. if (empty($value)) {
  511. return 'false';
  512. } else {
  513. return 'true';
  514. }
  515. }
  516. /**
  517. * xml_value_int
  518. *
  519. * @param xxx $tags
  520. * @param xxx $more_tags (optional, default=null)
  521. * @param xxx $default (optional, default='')
  522. * @return xxx
  523. */
  524. function xml_value_int($tags, $more_tags=null, $default=0) {
  525. $value = $this->xml_value($tags, $more_tags, $default, false);
  526. return intval($value);
  527. }
  528. /**
  529. * xml_value_js
  530. *
  531. * Note: html entities in captions (e.g. messages and button text)
  532. * do not need to be converted to javascript "\u" encoding
  533. * but those in question/answer arrays, generally do
  534. *
  535. * @param xxx $tags
  536. * @param xxx $more_tags (optional, default=null)
  537. * @param xxx $default (optional, default='')
  538. * @param xxx $convert_to_unicode (optional, default=false)
  539. * @return xxx
  540. */
  541. function xml_value_js($tags, $more_tags=null, $default='', $nl2br=true, $convert_to_unicode=true) {
  542. $value = $this->xml_value($tags, $more_tags, $default, $nl2br);
  543. return $this->js_value_safe($value, $convert_to_unicode);
  544. }
  545. /**
  546. * js_value_safe
  547. *
  548. * @param xxx $str
  549. * @param xxx $convert_to_unicode (optional, default=false)
  550. * @return xxx
  551. */
  552. function js_value_safe($str, $convert_to_unicode=false) {
  553. // encode a string for javascript
  554. static $replace_pairs = array(
  555. // backslashes and quotes
  556. '\\'=>'\\\\', "'"=>"\\'", '"'=>'\\"',
  557. // newlines (win = "\r\n", mac="\r", linux/unix="\n")
  558. "\r\n"=>'\\n', "\r"=>'\\n', "\n"=>'\\n',
  559. // other (closing tag is for XHTML compliance)
  560. "\0"=>'\\0', '</'=>'<\\/'
  561. );
  562. // convert unicode chars to html entities, if required
  563. // Note that this will also decode named entities such as &apos; and &quot;
  564. // so we have to put "strtr()" AFTER this call to textlib::utf8_to_entities()
  565. if ($convert_to_unicode) {
  566. $str = hotpot_textlib('utf8_to_entities', $str, false, true);
  567. }
  568. $str = strtr($str, $replace_pairs);
  569. // convert (hex and decimal) html entities to javascript unicode, if required
  570. if ($convert_to_unicode) {
  571. $search = '/&#x([0-9A-F]+);/i';
  572. $callback = array($this, 'js_unicode_char');
  573. $str = preg_replace_callback($search, $callback, $str);
  574. }
  575. return $str;
  576. }
  577. /**
  578. * js_unicode_char
  579. *
  580. * @param xxx $match
  581. * @return xxx
  582. */
  583. function js_unicode_char($match) {
  584. return sprintf('\\u%04s', $match[1]);
  585. }
  586. /**
  587. * synchronize_moodle_settings
  588. *
  589. * @param xxx $hotpot (passed by reference)
  590. * @return xxx
  591. */
  592. function synchronize_moodle_settings(&$hotpot) {
  593. $name = $this->get_name();
  594. if ($name=='' || $name==$hotpot->name) {
  595. return false;
  596. } else {
  597. $hotpot->name = $name;
  598. return true;
  599. }
  600. }
  601. } // end class