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

/include/functions.php

https://github.com/tremby/questionbank
PHP | 445 lines | 354 code | 39 blank | 52 comment | 39 complexity | 6991d73cf067096c091bb0bf0f9f1e33 MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception
  1. <?php
  2. /*
  3. * Question Bank
  4. */
  5. /*------------------------------------------------------------------------------
  6. (c) 2010 JISC-funded EASiHE project, University of Southampton
  7. Licensed under the Creative Commons 'Attribution non-commercial share alike'
  8. licence -- see the LICENCE file for more details
  9. ------------------------------------------------------------------------------*/
  10. function forbidden($message = "403: forbidden", $mimetype = "text/plain") {
  11. header("Content-Type: $mimetype", true, 403);
  12. echo $message;
  13. exit;
  14. }
  15. // return the database object, connecting and setting up the schema first if
  16. // necessary
  17. function db() {
  18. if (array_key_exists("db", $GLOBALS))
  19. return $GLOBALS["db"];
  20. $GLOBALS["db"] = new SQLite3((basename(SITEROOT_LOCAL) == "eqiat" ? dirname(SITEROOT_LOCAL) . "/" : SITEROOT_LOCAL) . "db/db.sqlite");
  21. $GLOBALS["db"]->exec("
  22. BEGIN TRANSACTION;
  23. CREATE TABLE IF NOT EXISTS items (
  24. identifier TEXT PRIMARY KEY ASC NOT NULL,
  25. uploaded INTEGER NOT NULL,
  26. modified INTEGER NULL,
  27. user TEXT NOT NULL,
  28. title TEXT NOT NULL,
  29. description TEXT NULL,
  30. xml BLOB NOT NULL
  31. );
  32. CREATE INDEX IF NOT EXISTS items_user ON items (user ASC);
  33. CREATE TABLE IF NOT EXISTS keywords (
  34. item TEXT NOT NULL,
  35. keyword TEXT NOT NULL
  36. );
  37. CREATE INDEX IF NOT EXISTS keywords_item ON keywords (item ASC);
  38. CREATE INDEX IF NOT EXISTS keywords_keyword ON keywords (keyword ASC);
  39. CREATE TABLE IF NOT EXISTS users (
  40. username TEXT PRIMARY KEY ASC NOT NULL,
  41. passwordhash TEXT NOT NULL,
  42. registered INTEGER NOT NULL,
  43. privileges INTEGER NOT NULL DEFAULT 0,
  44. deleted INTEGER NOT NULL DEFAULT 0
  45. );
  46. CREATE TABLE IF NOT EXISTS ratings (
  47. user TEXT NOT NULL,
  48. item TEXT NOT NULL,
  49. rating INTEGER NOT NULL,
  50. posted INTEGER NOT NULL
  51. );
  52. CREATE INDEX IF NOT EXISTS ratings_item ON ratings (item ASC);
  53. CREATE INDEX IF NOT EXISTS ratings_posted ON ratings (posted ASC);
  54. CREATE TABLE IF NOT EXISTS comments (
  55. user TEXT NOT NULL,
  56. item TEXT NOT NULL,
  57. comment TEXT NOT NULL,
  58. posted INTEGER NOT NULL
  59. );
  60. CREATE INDEX IF NOT EXISTS comments_item ON comments (item ASC);
  61. COMMIT;
  62. ");
  63. return $GLOBALS["db"];
  64. }
  65. // return true if a user exists in the database
  66. // if checking a password, deleted users do not exist (therefore a deleted user
  67. // can't log in)
  68. // otherwise, deleted users do exist (therefore a new user can't register with a
  69. // previously used username)
  70. function userexists($username, $password = null, $ishash = false) {
  71. if (!is_null($password) && !$ishash)
  72. $password = md5($password);
  73. $query = "SELECT COUNT(*) FROM users WHERE username LIKE '" . db()->escapeString($username) . "'";
  74. if (!is_null($password))
  75. $query .= " AND passwordhash='" . db()->escapeString($password) . "' AND deleted=0";
  76. return db()->querySingle($query) === 1;
  77. }
  78. // return true if a user exists but has been deleted
  79. function userdeleted($username) {
  80. return (boolean) db()->querySingle("SELECT COUNT(*) FROM users WHERE username LIKE '" . db()->escapeString($username) . "' AND deleted=1;");
  81. }
  82. // return true if the user (named or current) has raised privileges
  83. function userhasprivileges($user = null) {
  84. if (is_null($user)) {
  85. if (!loggedin())
  86. return false;
  87. $user = username();
  88. }
  89. if (!userexists($user)) {
  90. echo "user doesn't exist";
  91. return false;
  92. }
  93. return (boolean) db()->querySingle("SELECT privileges FROM users WHERE username='" . db()->escapeString($user) . "';");
  94. }
  95. // return a count of privileged users
  96. function privilegedusers() {
  97. return db()->querySingle("SELECT COUNT(*) FROM users WHERE privileges=1;");
  98. }
  99. // attempt to log in
  100. function login($username, $password, $ishash = false) {
  101. if (userexists($username, $password, $ishash)) {
  102. $_SESSION[SITE_TITLE . "_username"] = $username;
  103. $_SESSION[SITE_TITLE . "_passwordhash"] = $ishash ? $password : md5($password);
  104. return true;
  105. }
  106. return false;
  107. }
  108. // log out
  109. function logout() {
  110. unset($_SESSION[SITE_TITLE . "_username"], $_SESSION[SITE_TITLE . "_passwordhash"]);
  111. }
  112. // user is logged in
  113. function loggedin() {
  114. return isset($_SESSION[SITE_TITLE . "_username"]) && isset($_SESSION[SITE_TITLE . "_passwordhash"]) && userexists($_SESSION[SITE_TITLE . "_username"], $_SESSION[SITE_TITLE . "_passwordhash"], true);
  115. }
  116. // return username or false if not logged in
  117. function username() {
  118. if (loggedin())
  119. return $_SESSION[SITE_TITLE . "_username"];
  120. return false;
  121. }
  122. // if a user is not logged in, show a login form and exit or, if async, send 403 forbidden
  123. function requirelogin() {
  124. if (loggedin())
  125. return;
  126. if (isset($_REQUEST["async"]))
  127. forbidden();
  128. $_SESSION["nextpage"] = $_SERVER["REQUEST_URI"];
  129. include "content/login.php";
  130. exit;
  131. }
  132. // return true if an item with the given identifier exists in the database
  133. function itemexists($qtiid) {
  134. return db()->querySingle("SELECT COUNT(*) FROM items WHERE identifier='" . db()->escapeString($qtiid) . "';") === 1;
  135. }
  136. // return the owner of an item with the given identifier
  137. function itemowner($qtiid) {
  138. return db()->querySingle("SELECT user FROM items WHERE identifier='" . db()->escapeString($qtiid) . "';");
  139. }
  140. // return the item with the given identifier from the database
  141. function getitem($qtiid) {
  142. if (!itemexists($qtiid))
  143. return false;
  144. // get item
  145. $item = db()->querySingle("SELECT * FROM items WHERE identifier='" . db()->escapeString($qtiid) . "';", true);
  146. // get keywords
  147. $item["keywords"] = array();
  148. $result = db()->query("SELECT keyword FROM keywords WHERE item='" . db()->escapeString($qtiid) . "' ORDER BY keyword ASC;");
  149. while ($row = $result->fetchArray(SQLITE3_NUM))
  150. $item["keywords"][] = $row[0];
  151. // get rating (since last modification)
  152. $rating = db()->querySingle("
  153. SELECT AVG(rating) AS rating, COUNT(rating) AS ratingcount
  154. FROM ratings
  155. WHERE item='" . db()->escapeString($qtiid) . "'
  156. AND posted > " . max($item["uploaded"], is_null($item["modified"]) ? 0 : $item["modified"]) . "
  157. ;", true);
  158. $item["ratingcount"] = $rating["ratingcount"];
  159. $item["rating"] = $rating["ratingcount"] > 0 ? $rating["rating"] : null;
  160. // get comments
  161. $item["comments"] = array();
  162. $result = db()->query("
  163. SELECT
  164. comments.user AS user,
  165. comments.comment AS comment,
  166. comments.posted AS posted,
  167. ratings.rating AS rating,
  168. users.deleted AS userdeleted
  169. FROM comments
  170. LEFT JOIN ratings ON comments.user=ratings.user AND comments.item=ratings.item AND comments.posted=ratings.posted
  171. LEFT JOIN users ON comments.user=users.username
  172. WHERE comments.item='" . db()->escapeString($qtiid) . "'
  173. ORDER BY posted ASC;
  174. ");
  175. while ($row = $result->fetchArray(SQLITE3_ASSOC))
  176. $item["comments"][] = $row;
  177. return $item;
  178. }
  179. // return the current user's rating of a given item
  180. function itemrating($qtiid) {
  181. if (!loggedin() || !itemexists($qtiid))
  182. return false;
  183. $item = getitem($qtiid);
  184. $result = db()->query("
  185. SELECT rating
  186. FROM ratings
  187. WHERE item='" . db()->escapeString($qtiid) . "'
  188. AND posted > " . max($item["uploaded"], is_null($item["modified"]) ? 0 : $item["modified"]) . "
  189. AND user='" . db()->escapeString(username()) . "'
  190. ;");
  191. if ($row = $result->fetchArray(SQLITE3_NUM))
  192. return $row[0];
  193. return null;
  194. }
  195. // turn a string of xhtml into html
  196. function xhtml_to_html($xhtml) {
  197. $selfclosing = array(
  198. "area",
  199. "base",
  200. "basefont",
  201. "br",
  202. "col",
  203. "frame",
  204. "hr",
  205. "img",
  206. "input",
  207. "link",
  208. "meta",
  209. "param",
  210. );
  211. // HTML's self closing tags don't need to be closed
  212. // (catch both <tag.../> and <tag...></tag>)
  213. $html = preg_replace('%<(' . implode("|", $selfclosing) . ')\b([^>]*?)\s*(/>|>\s*</\1>)%i', '<\1\2>', $xhtml);
  214. // other empty tags in the short style (eg <div/>) need to be opened and
  215. // closed
  216. $html = preg_replace('%<(.+?)\b([^>]*?)\s*/>%', '<\1\2></\1>', $html);
  217. // get rid of any xhtml namespace tags
  218. $html = preg_replace('%\s+xmlns=(["\'])http://www.w3.org/1999/xhtml\1%i', '', $html);
  219. return $html;
  220. }
  221. // given the page SimpleXML element of a response from QTIEngine, extract the
  222. // important bits of the header (javascript and stylesheet links) and return
  223. // them as an HTML string to be put in the header
  224. function qtiengine_header_html(SimpleXMLElement $page) {
  225. $headerextra = "";
  226. // javascript
  227. foreach ($page->html->head->script as $script) {
  228. if (isset($script["src"]) && isset($script["type"]) && ((string) $script["type"] == "text/javascript" || (string) $script["type"] == "application/javascript")) {
  229. // TODO: cater for inline scripts as well as included ones
  230. ob_start();
  231. ?>
  232. <script type="text/javascript" src="<?php echo (string) $script["src"]; ?>"></script>
  233. <?php
  234. $headerextra .= ob_get_clean();
  235. }
  236. }
  237. // stylesheets
  238. foreach ($page->html->head->link as $link) {
  239. if (isset($link["rel"]) && (string) $link["rel"] == "stylesheet" && isset($link["href"]) && isset($link["type"]) && (string) $link["type"] == "text/css") {
  240. // TODO: cater for inline styles as well as included ones
  241. ob_start();
  242. ?>
  243. <link rel="stylesheet" type="text/css"<?php if (isset($link["media"])) { ?> media="<?php echo (string) $link["media"]; ?>"<?php } ?> href="<?php echo (string) $link["href"]; ?>">
  244. <?php
  245. $headerextra .= ob_get_clean();
  246. }
  247. }
  248. return $headerextra;
  249. }
  250. // given the page SimpleXML element of a response from QTIEngine, extract the
  251. // div with id "body" and convert to HTML
  252. // the default QTIEngine XSL transformation has a div with everything we want in
  253. // it with id "body" (it doesn't include the internal state etc)
  254. // also replace h2 tags with h3 and strip hr tags
  255. // also add [] to the end of the name attributes of checkboxes if it's not
  256. // already there so that PHP receives them back properly when posted
  257. function qtiengine_bodydiv_html(SimpleXMLElement $page, $divid = "qtienginebodydiv") {
  258. // php5's support for default namespace is useless so we have to define it
  259. // manually
  260. $namespaces = $page->html->getNamespaces();
  261. $defaultnamespace = $namespaces[""];
  262. $page->registerXPathNamespace("n", $defaultnamespace);
  263. $bodydivs = $page->xpath("//n:div[@id='body']");
  264. if (count($bodydivs) != 1)
  265. servererror("didn't get expected HTML output from QTIEngine");
  266. $bodydiv = $bodydivs[0];
  267. $bodydiv["id"] = $divid;
  268. foreach ($page->xpath("//n:input[@type='checkbox']") as $checkbox)
  269. if (!preg_match('%\]$%', (string) $checkbox["name"]))
  270. $checkbox["name"] = (string) $checkbox["name"] . "[]";
  271. return preg_replace(array('%<(/?)h2\b%', '%<hr\b.*?>%'), array('<\1h3', ''), xhtml_to_html(simplexml_indented_string($bodydiv)));
  272. }
  273. function callstack($html = true) {
  274. $bt = debug_backtrace();
  275. // lose the call to this function
  276. array_shift($bt);
  277. echo "backtrace:\n";
  278. if ($html) echo "<ul>";
  279. foreach ($bt as $call) {
  280. if ($html) echo "<li>";
  281. echo $call["file"] . ":" . $call["line"] . " calls " . $call["function"] . "(" . implode(", ", $call["args"]) . ")";
  282. if ($html)
  283. echo "</li>";
  284. else
  285. echo "\n";
  286. }
  287. if ($html) echo "</ul>";
  288. }
  289. // put an item in the database
  290. // doesn't check who the owner is
  291. // updates it or uploads it depending on whether it was already there.
  292. // arguments:
  293. // QTIAssessmentItem object
  294. // or string XML and metadata array
  295. // or SimpleXML object and metadata array
  296. function deposititem() {
  297. $args = func_get_args();
  298. // collect data
  299. switch (count($args)) {
  300. case 1:
  301. if (!($args[0] instanceof QTIAssessmentItem))
  302. throw new Exception("with one argument, expected QTIAssessmentItem");
  303. $identifier = $args[0]->getQTIID();
  304. $title = $args[0]->data("title");
  305. $description = $args[0]->data("description");
  306. $xml = $args[0]->getQTIIndentedString();
  307. $keywords = $args[0]->getKeywords();
  308. break;
  309. case 2:
  310. if ($args[0] instanceof SimpleXMLElement) {
  311. $sxml = $args[0];
  312. $xml = simplexml_indented_string($args[0]);
  313. } else if (is_string($args[0])) {
  314. $sxml = simplexml_load_string($args[0]);
  315. $xml = $args[0];
  316. } else
  317. throw new Exception("With two arguments expected SimpleXML element or XML string as first");
  318. $identifier = (string) $sxml["identifier"];
  319. $title = (string) $sxml["title"];
  320. if (!is_array($args[1]))
  321. throw new Exception("With two arguments expected metadata array as second");
  322. $description = isset($args[1]["description"]) ? $args[1]["description"] : "";
  323. $keywords = isset($args[1]["keywords"]) ? $args[1]["keywords"] : array();
  324. break;
  325. default:
  326. throw new Exception("expected one or two arguments");
  327. }
  328. db()->exec("BEGIN TRANSACTION;");
  329. // update or insert
  330. if (itemexists($identifier)) {
  331. // update item
  332. db()->exec("
  333. DELETE FROM keywords WHERE item='" . db()->escapeString($identifier) . "';
  334. UPDATE items SET
  335. modified=" . time() . ",
  336. title='" . db()->escapeString($title) . "',
  337. description='" . db()->escapeString($description) . "',
  338. xml='" . db()->escapeString($xml) . "'
  339. WHERE identifier='" . db()->escapeString($identifier) . "';
  340. ");
  341. // add a comment to the item to show it has been updated
  342. db()->exec("
  343. INSERT INTO comments VALUES (
  344. '" . db()->escapeString(username()) . "',
  345. '" . db()->escapeString($identifier) . "',
  346. '" . db()->escapeString("Automatic comment: this item has been updated") . "',
  347. " . time() . "
  348. );
  349. ");
  350. } else {
  351. // new item -- insert it
  352. db()->exec("
  353. INSERT INTO items VALUES (
  354. '" . db()->escapeString($identifier) . "',
  355. " . time() . ",
  356. NULL,
  357. '" . db()->escapeString(username()) . "',
  358. '" . db()->escapeString($title) . "',
  359. '" . db()->escapeString($description) . "',
  360. '" . db()->escapeString($xml) . "'
  361. );
  362. ");
  363. }
  364. // add keywords
  365. foreach ($keywords as $keyword) {
  366. db()->exec("
  367. INSERT INTO keywords VALUES (
  368. '" . db()->escapeString($identifier) . "',
  369. '" . db()->escapeString($keyword) . "'
  370. );
  371. ");
  372. }
  373. // commit changes
  374. db()->exec("COMMIT;");
  375. }
  376. // return true if the given QTI (SimpleXML or string) was authored in Eqiat
  377. function authoredineqiat($xml) {
  378. if (!($xml instanceof SimpleXMLElement))
  379. $xml = simplexml_load_string($xml);
  380. if (!$xml)
  381. return false;
  382. return isset($xml["toolName"]) && (string) $xml["toolName"] == "Eqiat";
  383. }
  384. ?>