[ Index ] |
PHP Cross Reference of Unnamed Project |
[Summary view] [Print] [Text view]
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 /** 18 * Library of functions and constants for module chat 19 * 20 * @package mod_chat 21 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 25 defined('MOODLE_INTERNAL') || die(); 26 27 require_once($CFG->dirroot.'/calendar/lib.php'); 28 29 // The HTML head for the message window to start with (<!-- nix --> is used to get some browsers starting with output. 30 global $CHAT_HTMLHEAD; 31 $CHAT_HTMLHEAD = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\" \"http://www.w3.org/TR/REC-html40/loose.dtd\"><html><head></head>\n<body>\n\n".padding(200); 32 33 // The HTML head for the message window to start with (with js scrolling). 34 global $CHAT_HTMLHEAD_JS; 35 $CHAT_HTMLHEAD_JS = <<<EOD 36 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"> 37 <html><head><script type="text/javascript"> 38 //<![CDATA[ 39 function move() { 40 if (scroll_active) 41 window.scroll(1,400000); 42 window.setTimeout("move()",100); 43 } 44 var scroll_active = true; 45 move(); 46 //]]> 47 </script> 48 </head> 49 <body onBlur="scroll_active = true" onFocus="scroll_active = false"> 50 EOD; 51 global $CHAT_HTMLHEAD_JS; 52 $CHAT_HTMLHEAD_JS .= padding(200); 53 54 // The HTML code for standard empty pages (e.g. if a user was kicked out). 55 global $CHAT_HTMLHEAD_OUT; 56 $CHAT_HTMLHEAD_OUT = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\" \"http://www.w3.org/TR/REC-html40/loose.dtd\"><html><head><title>You are out!</title></head><body></body></html>"; 57 58 // The HTML head for the message input page. 59 global $CHAT_HTMLHEAD_MSGINPUT; 60 $CHAT_HTMLHEAD_MSGINPUT = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\" \"http://www.w3.org/TR/REC-html40/loose.dtd\"><html><head><title>Message Input</title></head><body>"; 61 62 // The HTML code for the message input page, with JavaScript. 63 global $CHAT_HTMLHEAD_MSGINPUT_JS; 64 $CHAT_HTMLHEAD_MSGINPUT_JS = <<<EOD 65 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"> 66 <html> 67 <head><title>Message Input</title> 68 <script type="text/javascript"> 69 //<![CDATA[ 70 scroll_active = true; 71 function empty_field_and_submit() { 72 document.fdummy.arsc_message.value=document.f.arsc_message.value; 73 document.fdummy.submit(); 74 document.f.arsc_message.focus(); 75 document.f.arsc_message.select(); 76 return false; 77 } 78 //]]> 79 </script> 80 </head><body OnLoad="document.f.arsc_message.focus();document.f.arsc_message.select();">; 81 EOD; 82 83 // Dummy data that gets output to the browser as needed, in order to make it show output. 84 global $CHAT_DUMMY_DATA; 85 $CHAT_DUMMY_DATA = padding(200); 86 87 /** 88 * @param int $n 89 * @return string 90 */ 91 function padding($n) { 92 $str = ''; 93 for ($i = 0; $i < $n; $i++) { 94 $str .= "<!-- nix -->\n"; 95 } 96 return $str; 97 } 98 99 /** 100 * Given an object containing all the necessary data, 101 * (defined by the form in mod_form.php) this function 102 * will create a new instance and return the id number 103 * of the new instance. 104 * 105 * @global object 106 * @param object $chat 107 * @return int 108 */ 109 function chat_add_instance($chat) { 110 global $DB; 111 112 $chat->timemodified = time(); 113 114 $returnid = $DB->insert_record("chat", $chat); 115 116 $event = new stdClass(); 117 $event->name = $chat->name; 118 $event->description = format_module_intro('chat', $chat, $chat->coursemodule); 119 $event->courseid = $chat->course; 120 $event->groupid = 0; 121 $event->userid = 0; 122 $event->modulename = 'chat'; 123 $event->instance = $returnid; 124 $event->eventtype = 'chattime'; 125 $event->timestart = $chat->chattime; 126 $event->timeduration = 0; 127 128 calendar_event::create($event); 129 130 return $returnid; 131 } 132 133 /** 134 * Given an object containing all the necessary data, 135 * (defined by the form in mod_form.php) this function 136 * will update an existing instance with new data. 137 * 138 * @global object 139 * @param object $chat 140 * @return bool 141 */ 142 function chat_update_instance($chat) { 143 global $DB; 144 145 $chat->timemodified = time(); 146 $chat->id = $chat->instance; 147 148 $DB->update_record("chat", $chat); 149 150 $event = new stdClass(); 151 152 if ($event->id = $DB->get_field('event', 'id', array('modulename' => 'chat', 'instance' => $chat->id))) { 153 154 $event->name = $chat->name; 155 $event->description = format_module_intro('chat', $chat, $chat->coursemodule); 156 $event->timestart = $chat->chattime; 157 158 $calendarevent = calendar_event::load($event->id); 159 $calendarevent->update($event); 160 } 161 162 return true; 163 } 164 165 /** 166 * Given an ID of an instance of this module, 167 * this function will permanently delete the instance 168 * and any data that depends on it. 169 * 170 * @global object 171 * @param int $id 172 * @return bool 173 */ 174 function chat_delete_instance($id) { 175 global $DB; 176 177 if (! $chat = $DB->get_record('chat', array('id' => $id))) { 178 return false; 179 } 180 181 $result = true; 182 183 // Delete any dependent records here. 184 185 if (! $DB->delete_records('chat', array('id' => $chat->id))) { 186 $result = false; 187 } 188 if (! $DB->delete_records('chat_messages', array('chatid' => $chat->id))) { 189 $result = false; 190 } 191 if (! $DB->delete_records('chat_messages_current', array('chatid' => $chat->id))) { 192 $result = false; 193 } 194 if (! $DB->delete_records('chat_users', array('chatid' => $chat->id))) { 195 $result = false; 196 } 197 198 if (! $DB->delete_records('event', array('modulename' => 'chat', 'instance' => $chat->id))) { 199 $result = false; 200 } 201 202 return $result; 203 } 204 205 /** 206 * Given a course and a date, prints a summary of all chat rooms past and present 207 * This function is called from block_recent_activity 208 * 209 * @global object 210 * @global object 211 * @global object 212 * @param object $course 213 * @param bool $viewfullnames 214 * @param int|string $timestart Timestamp 215 * @return bool 216 */ 217 function chat_print_recent_activity($course, $viewfullnames, $timestart) { 218 global $CFG, $USER, $DB, $OUTPUT; 219 220 // This is approximate only, but it is really fast. 221 $timeout = $CFG->chat_old_ping * 10; 222 223 if (!$mcms = $DB->get_records_sql("SELECT cm.id, MAX(chm.timestamp) AS lasttime 224 FROM {course_modules} cm 225 JOIN {modules} md ON md.id = cm.module 226 JOIN {chat} ch ON ch.id = cm.instance 227 JOIN {chat_messages} chm ON chm.chatid = ch.id 228 WHERE chm.timestamp > ? AND ch.course = ? AND md.name = 'chat' 229 GROUP BY cm.id 230 ORDER BY lasttime ASC", array($timestart, $course->id))) { 231 return false; 232 } 233 234 $past = array(); 235 $current = array(); 236 $modinfo = get_fast_modinfo($course); // Reference needed because we might load the groups. 237 238 foreach ($mcms as $cmid => $mcm) { 239 if (!array_key_exists($cmid, $modinfo->cms)) { 240 continue; 241 } 242 $cm = $modinfo->cms[$cmid]; 243 if (!$modinfo->cms[$cm->id]->uservisible) { 244 continue; 245 } 246 247 if (groups_get_activity_groupmode($cm) != SEPARATEGROUPS 248 or has_capability('moodle/site:accessallgroups', context_module::instance($cm->id))) { 249 if ($timeout > time() - $mcm->lasttime) { 250 $current[] = $cm; 251 } else { 252 $past[] = $cm; 253 } 254 255 continue; 256 } 257 258 // Verify groups in separate mode. 259 if (!$mygroupids = $modinfo->get_groups($cm->groupingid)) { 260 continue; 261 } 262 263 // Ok, last post was not for my group - we have to query db to get last message from one of my groups. 264 // The only minor problem is that the order will not be correct. 265 $mygroupids = implode(',', $mygroupids); 266 267 if (!$mcm = $DB->get_record_sql("SELECT cm.id, MAX(chm.timestamp) AS lasttime 268 FROM {course_modules} cm 269 JOIN {chat} ch ON ch.id = cm.instance 270 JOIN {chat_messages_current} chm ON chm.chatid = ch.id 271 WHERE chm.timestamp > ? AND cm.id = ? AND 272 (chm.groupid IN ($mygroupids) OR chm.groupid = 0) 273 GROUP BY cm.id", array($timestart, $cm->id))) { 274 continue; 275 } 276 277 $mcms[$cmid]->lasttime = $mcm->lasttime; 278 if ($timeout > time() - $mcm->lasttime) { 279 $current[] = $cm; 280 } else { 281 $past[] = $cm; 282 } 283 } 284 285 if (!$past and !$current) { 286 return false; 287 } 288 289 $strftimerecent = get_string('strftimerecent'); 290 291 if ($past) { 292 echo $OUTPUT->heading(get_string("pastchats", 'chat').':', 3); 293 294 foreach ($past as $cm) { 295 $link = $CFG->wwwroot.'/mod/chat/view.php?id='.$cm->id; 296 $date = userdate($mcms[$cm->id]->lasttime, $strftimerecent); 297 echo '<div class="head"><div class="date">'.$date.'</div></div>'; 298 echo '<div class="info"><a href="'.$link.'">'.format_string($cm->name, true).'</a></div>'; 299 } 300 } 301 302 if ($current) { 303 echo $OUTPUT->heading(get_string("currentchats", 'chat').':', 3); 304 305 $oldest = floor((time() - $CFG->chat_old_ping) / 10) * 10; // Better db caching. 306 307 $timeold = time() - $CFG->chat_old_ping; 308 $timeold = floor($timeold / 10) * 10; // Better db caching. 309 $timeoldext = time() - ($CFG->chat_old_ping * 10); // JSless gui_basic needs much longer timeouts. 310 $timeoldext = floor($timeoldext / 10) * 10; // Better db caching. 311 312 $params = array('timeold' => $timeold, 'timeoldext' => $timeoldext, 'cmid' => $cm->id); 313 314 $timeout = "AND ((chu.version<>'basic' AND chu.lastping>:timeold) OR (chu.version='basic' AND chu.lastping>:timeoldext))"; 315 316 foreach ($current as $cm) { 317 // Count users first. 318 $mygroupids = $modinfo->groups[$cm->groupingid]; 319 if (!empty($mygroupids)) { 320 list($subquery, $subparams) = $DB->get_in_or_equal($mygroupids, SQL_PARAMS_NAMED, 'gid'); 321 $params += $subparams; 322 $groupselect = "AND (chu.groupid $subquery OR chu.groupid = 0)"; 323 } else { 324 $groupselect = ""; 325 } 326 327 $userfields = user_picture::fields('u'); 328 if (!$users = $DB->get_records_sql("SELECT $userfields 329 FROM {course_modules} cm 330 JOIN {chat} ch ON ch.id = cm.instance 331 JOIN {chat_users} chu ON chu.chatid = ch.id 332 JOIN {user} u ON u.id = chu.userid 333 WHERE cm.id = :cmid $timeout $groupselect 334 GROUP BY $userfields", $params)) { 335 } 336 337 $link = $CFG->wwwroot.'/mod/chat/view.php?id='.$cm->id; 338 $date = userdate($mcms[$cm->id]->lasttime, $strftimerecent); 339 340 echo '<div class="head"><div class="date">'.$date.'</div></div>'; 341 echo '<div class="info"><a href="'.$link.'">'.format_string($cm->name, true).'</a></div>'; 342 echo '<div class="userlist">'; 343 if ($users) { 344 echo '<ul>'; 345 foreach ($users as $user) { 346 echo '<li>'.fullname($user, $viewfullnames).'</li>'; 347 } 348 echo '</ul>'; 349 } 350 echo '</div>'; 351 } 352 } 353 354 return true; 355 } 356 357 /** 358 * Function to be run periodically according to the moodle cron 359 * This function searches for things that need to be done, such 360 * as sending out mail, toggling flags etc ... 361 * 362 * @global object 363 * @return bool 364 */ 365 function chat_cron () { 366 global $DB; 367 368 chat_update_chat_times(); 369 370 chat_delete_old_users(); 371 372 // Delete old messages with a single SQL query. 373 $subselect = "SELECT c.keepdays 374 FROM {chat} c 375 WHERE c.id = {chat_messages}.chatid"; 376 377 $sql = "DELETE 378 FROM {chat_messages} 379 WHERE ($subselect) > 0 AND timestamp < ( ".time()." -($subselect) * 24 * 3600)"; 380 381 $DB->execute($sql); 382 383 $sql = "DELETE 384 FROM {chat_messages_current} 385 WHERE timestamp < ( ".time()." - 8 * 3600)"; 386 387 $DB->execute($sql); 388 389 return true; 390 } 391 392 /** 393 * This standard function will check all instances of this module 394 * and make sure there are up-to-date events created for each of them. 395 * If courseid = 0, then every chat event in the site is checked, else 396 * only chat events belonging to the course specified are checked. 397 * This function is used, in its new format, by restore_refresh_events() 398 * 399 * @global object 400 * @param int $courseid 401 * @return bool 402 */ 403 function chat_refresh_events($courseid = 0) { 404 global $DB; 405 406 if ($courseid) { 407 if (! $chats = $DB->get_records("chat", array("course" => $courseid))) { 408 return true; 409 } 410 } else { 411 if (! $chats = $DB->get_records("chat")) { 412 return true; 413 } 414 } 415 $moduleid = $DB->get_field('modules', 'id', array('name' => 'chat')); 416 417 foreach ($chats as $chat) { 418 $cm = get_coursemodule_from_instance('chat', $chat->id, $chat->course); 419 $event = new stdClass(); 420 $event->name = $chat->name; 421 $event->description = format_module_intro('chat', $chat, $cm->id); 422 $event->timestart = $chat->chattime; 423 424 if ($event->id = $DB->get_field('event', 'id', array('modulename' => 'chat', 'instance' => $chat->id))) { 425 $calendarevent = calendar_event::load($event->id); 426 $calendarevent->update($event); 427 } else { 428 $event->courseid = $chat->course; 429 $event->groupid = 0; 430 $event->userid = 0; 431 $event->modulename = 'chat'; 432 $event->instance = $chat->id; 433 $event->eventtype = 'chattime'; 434 $event->timeduration = 0; 435 $event->visible = $DB->get_field('course_modules', 'visible', array('module' => $moduleid, 'instance' => $chat->id)); 436 437 calendar_event::create($event); 438 } 439 } 440 return true; 441 } 442 443 // Functions that require some SQL. 444 445 /** 446 * @global object 447 * @param int $chatid 448 * @param int $groupid 449 * @param int $groupingid 450 * @return array 451 */ 452 function chat_get_users($chatid, $groupid=0, $groupingid=0) { 453 global $DB; 454 455 $params = array('chatid' => $chatid, 'groupid' => $groupid, 'groupingid' => $groupingid); 456 457 if ($groupid) { 458 $groupselect = " AND (c.groupid=:groupid OR c.groupid='0')"; 459 } else { 460 $groupselect = ""; 461 } 462 463 if (!empty($groupingid)) { 464 $groupingjoin = "JOIN {groups_members} gm ON u.id = gm.userid 465 JOIN {groupings_groups} gg ON gm.groupid = gg.groupid AND gg.groupingid = :groupingid "; 466 467 } else { 468 $groupingjoin = ''; 469 } 470 471 $ufields = user_picture::fields('u'); 472 return $DB->get_records_sql("SELECT DISTINCT $ufields, c.lastmessageping, c.firstping 473 FROM {chat_users} c 474 JOIN {user} u ON u.id = c.userid $groupingjoin 475 WHERE c.chatid = :chatid $groupselect 476 ORDER BY c.firstping ASC", $params); 477 } 478 479 /** 480 * @global object 481 * @param int $chatid 482 * @param int $groupid 483 * @return array 484 */ 485 function chat_get_latest_message($chatid, $groupid=0) { 486 global $DB; 487 488 $params = array('chatid' => $chatid, 'groupid' => $groupid); 489 490 if ($groupid) { 491 $groupselect = "AND (groupid=:groupid OR groupid=0)"; 492 } else { 493 $groupselect = ""; 494 } 495 496 $sql = "SELECT * 497 FROM {chat_messages_current} WHERE chatid = :chatid $groupselect 498 ORDER BY timestamp DESC"; 499 500 // Return the lastest one message. 501 return $DB->get_record_sql($sql, $params, true); 502 } 503 504 /** 505 * login if not already logged in 506 * 507 * @global object 508 * @global object 509 * @param int $chatid 510 * @param string $version 511 * @param int $groupid 512 * @param object $course 513 * @return bool|int Returns the chat users sid or false 514 */ 515 function chat_login_user($chatid, $version, $groupid, $course) { 516 global $USER, $DB; 517 518 if (($version != 'sockets') and $chatuser = $DB->get_record('chat_users', array('chatid' => $chatid, 519 'userid' => $USER->id, 520 'groupid' => $groupid))) { 521 // This will update logged user information. 522 $chatuser->version = $version; 523 $chatuser->ip = $USER->lastip; 524 $chatuser->lastping = time(); 525 $chatuser->lang = current_language(); 526 527 // Sometimes $USER->lastip is not setup properly during login. 528 // Update with current value if possible or provide a dummy value for the db. 529 if (empty($chatuser->ip)) { 530 $chatuser->ip = getremoteaddr(); 531 } 532 533 if (($chatuser->course != $course->id) or ($chatuser->userid != $USER->id)) { 534 return false; 535 } 536 $DB->update_record('chat_users', $chatuser); 537 538 } else { 539 $chatuser = new stdClass(); 540 $chatuser->chatid = $chatid; 541 $chatuser->userid = $USER->id; 542 $chatuser->groupid = $groupid; 543 $chatuser->version = $version; 544 $chatuser->ip = $USER->lastip; 545 $chatuser->lastping = $chatuser->firstping = $chatuser->lastmessageping = time(); 546 $chatuser->sid = random_string(32); 547 $chatuser->course = $course->id; // Caching - needed for current_language too. 548 $chatuser->lang = current_language(); // Caching - to resource intensive to find out later. 549 550 // Sometimes $USER->lastip is not setup properly during login. 551 // Update with current value if possible or provide a dummy value for the db. 552 if (empty($chatuser->ip)) { 553 $chatuser->ip = getremoteaddr(); 554 } 555 556 $DB->insert_record('chat_users', $chatuser); 557 558 if ($version == 'sockets') { 559 // Do not send 'enter' message, chatd will do it. 560 } else { 561 chat_send_chatmessage($chatuser, 'enter', true); 562 } 563 } 564 565 return $chatuser->sid; 566 } 567 568 /** 569 * Delete the old and in the way 570 * 571 * @global object 572 * @global object 573 */ 574 function chat_delete_old_users() { 575 // Delete the old and in the way. 576 global $CFG, $DB; 577 578 $timeold = time() - $CFG->chat_old_ping; 579 $timeoldext = time() - ($CFG->chat_old_ping * 10); // JSless gui_basic needs much longer timeouts. 580 581 $query = "(version<>'basic' AND lastping<?) OR (version='basic' AND lastping<?)"; 582 $params = array($timeold, $timeoldext); 583 584 if ($oldusers = $DB->get_records_select('chat_users', $query, $params) ) { 585 $DB->delete_records_select('chat_users', $query, $params); 586 foreach ($oldusers as $olduser) { 587 chat_send_chatmessage($olduser, 'exit', true); 588 } 589 } 590 } 591 592 /** 593 * Updates chat records so that the next chat time is correct 594 * 595 * @global object 596 * @param int $chatid 597 * @return void 598 */ 599 function chat_update_chat_times($chatid=0) { 600 // Updates chat records so that the next chat time is correct. 601 global $DB; 602 603 $timenow = time(); 604 605 $params = array('timenow' => $timenow, 'chatid' => $chatid); 606 607 if ($chatid) { 608 if (!$chats[] = $DB->get_record_select("chat", "id = :chatid AND chattime <= :timenow AND schedule > 0", $params)) { 609 return; 610 } 611 } else { 612 if (!$chats = $DB->get_records_select("chat", "chattime <= :timenow AND schedule > 0", $params)) { 613 return; 614 } 615 } 616 617 foreach ($chats as $chat) { 618 switch ($chat->schedule) { 619 case 1: // Single event - turn off schedule and disable. 620 $chat->chattime = 0; 621 $chat->schedule = 0; 622 break; 623 case 2: // Repeat daily. 624 while ($chat->chattime <= $timenow) { 625 $chat->chattime += 24 * 3600; 626 } 627 break; 628 case 3: // Repeat weekly. 629 while ($chat->chattime <= $timenow) { 630 $chat->chattime += 7 * 24 * 3600; 631 } 632 break; 633 } 634 $DB->update_record("chat", $chat); 635 636 $event = new stdClass(); // Update calendar too. 637 638 $cond = "modulename='chat' AND instance = :chatid AND timestart <> :chattime"; 639 $params = array('chattime' => $chat->chattime, 'chatid' => $chat->id); 640 641 if ($event->id = $DB->get_field_select('event', 'id', $cond, $params)) { 642 $event->timestart = $chat->chattime; 643 $calendarevent = calendar_event::load($event->id); 644 $calendarevent->update($event, false); 645 } 646 } 647 } 648 649 /** 650 * Send a message on the chat. 651 * 652 * @param object $chatuser The chat user record. 653 * @param string $messagetext The message to be sent. 654 * @param bool $system False for non-system messages, true for system messages. 655 * @param object $cm The course module object, pass it to save a database query when we trigger the event. 656 * @return int The message ID. 657 * @since Moodle 2.6 658 */ 659 function chat_send_chatmessage($chatuser, $messagetext, $system = false, $cm = null) { 660 global $DB; 661 662 $message = new stdClass(); 663 $message->chatid = $chatuser->chatid; 664 $message->userid = $chatuser->userid; 665 $message->groupid = $chatuser->groupid; 666 $message->message = $messagetext; 667 $message->system = $system ? 1 : 0; 668 $message->timestamp = time(); 669 670 $messageid = $DB->insert_record('chat_messages', $message); 671 $DB->insert_record('chat_messages_current', $message); 672 $message->id = $messageid; 673 674 if (!$system) { 675 676 if (empty($cm)) { 677 $cm = get_coursemodule_from_instance('chat', $chatuser->chatid, $chatuser->course); 678 } 679 680 $params = array( 681 'context' => context_module::instance($cm->id), 682 'objectid' => $message->id, 683 // We set relateduserid, because when triggered from the chat daemon, the event userid is null. 684 'relateduserid' => $chatuser->userid 685 ); 686 $event = \mod_chat\event\message_sent::create($params); 687 $event->add_record_snapshot('chat_messages', $message); 688 $event->trigger(); 689 } 690 691 return $message->id; 692 } 693 694 /** 695 * @global object 696 * @global object 697 * @param object $message 698 * @param int $courseid 699 * @param object $sender 700 * @param object $currentuser 701 * @param string $chatlastrow 702 * @return bool|string Returns HTML or false 703 */ 704 function chat_format_message_manually($message, $courseid, $sender, $currentuser, $chatlastrow = null) { 705 global $CFG, $USER, $OUTPUT; 706 707 $output = new stdClass(); 708 $output->beep = false; // By default. 709 $output->refreshusers = false; // By default. 710 711 // Find the correct timezone for displaying this message. 712 $tz = core_date::get_user_timezone($currentuser); 713 714 $message->strtime = userdate($message->timestamp, get_string('strftimemessage', 'chat'), $tz); 715 716 $message->picture = $OUTPUT->user_picture($sender, array('size' => false, 'courseid' => $courseid, 'link' => false)); 717 718 if ($courseid) { 719 $message->picture = "<a onclick=\"window.open('$CFG->wwwroot/user/view.php?id=$sender->id&course=$courseid')\"". 720 " href=\"$CFG->wwwroot/user/view.php?id=$sender->id&course=$courseid\">$message->picture</a>"; 721 } 722 723 // Calculate the row class. 724 if ($chatlastrow !== null) { 725 $rowclass = ' class="r'.$chatlastrow.'" '; 726 } else { 727 $rowclass = ''; 728 } 729 730 // Start processing the message. 731 732 if (!empty($message->system)) { 733 // System event. 734 $output->text = $message->strtime.': '.get_string('message'.$message->message, 'chat', fullname($sender)); 735 $output->html = '<table class="chat-event"><tr'.$rowclass.'><td class="picture">'.$message->picture.'</td>'; 736 $output->html .= '<td class="text"><span class="event">'.$output->text.'</span></td></tr></table>'; 737 $output->basic = '<tr class="r1"> 738 <th scope="row" class="cell c1 title"></th> 739 <td class="cell c2 text">' . get_string('message'.$message->message, 'chat', fullname($sender)) . '</td> 740 <td class="cell c3">' . $message->strtime . '</td> 741 </tr>'; 742 if ($message->message == 'exit' or $message->message == 'enter') { 743 $output->refreshusers = true; // Force user panel refresh ASAP. 744 } 745 return $output; 746 } 747 748 // It's not a system event. 749 $text = trim($message->message); 750 751 // Parse the text to clean and filter it. 752 $options = new stdClass(); 753 $options->para = false; 754 $options->blanktarget = true; 755 $text = format_text($text, FORMAT_MOODLE, $options, $courseid); 756 757 // And now check for special cases. 758 $patternto = '#^\s*To\s([^:]+):(.*)#'; 759 $special = false; 760 761 if (substr($text, 0, 5) == 'beep ') { 762 // It's a beep! 763 $special = true; 764 $beepwho = trim(substr($text, 5)); 765 766 if ($beepwho == 'all') { // Everyone. 767 $outinfobasic = get_string('messagebeepseveryone', 'chat', fullname($sender)); 768 $outinfo = $message->strtime . ': ' . $outinfobasic; 769 $outmain = ''; 770 771 $output->beep = true; // Eventually this should be set to a filename uploaded by the user. 772 773 } else if ($beepwho == $currentuser->id) { // Current user. 774 $outinfobasic = get_string('messagebeepsyou', 'chat', fullname($sender)); 775 $outinfo = $message->strtime . ': ' . $outinfobasic; 776 $outmain = ''; 777 $output->beep = true; 778 779 } else { // Something is not caught? 780 return false; 781 } 782 } else if (substr($text, 0, 1) == '/') { // It's a user command. 783 $special = true; 784 $pattern = '#(^\/)(\w+).*#'; 785 preg_match($pattern, $text, $matches); 786 $command = isset($matches[2]) ? $matches[2] : false; 787 // Support some IRC commands. 788 switch ($command) { 789 case 'me': 790 $outinfo = $message->strtime; 791 $outmain = '*** <b>'.$sender->firstname.' '.substr($text, 4).'</b>'; 792 break; 793 default: 794 // Error, we set special back to false to use the classic message output. 795 $special = false; 796 break; 797 } 798 } else if (preg_match($patternto, $text)) { 799 $special = true; 800 $matches = array(); 801 preg_match($patternto, $text, $matches); 802 if (isset($matches[1]) && isset($matches[2])) { 803 $outinfo = $message->strtime; 804 $outmain = $sender->firstname.' '.get_string('saidto', 'chat').' <i>'.$matches[1].'</i>: '.$matches[2]; 805 } else { 806 // Error, we set special back to false to use the classic message output. 807 $special = false; 808 } 809 } 810 811 if (!$special) { 812 $outinfo = $message->strtime.' '.$sender->firstname; 813 $outmain = $text; 814 } 815 816 // Format the message as a small table. 817 818 $output->text = strip_tags($outinfo.': '.$outmain); 819 820 $output->html = "<table class=\"chat-message\"><tr$rowclass><td class=\"picture\" valign=\"top\">$message->picture</td>"; 821 $output->html .= "<td class=\"text\"><span class=\"title\">$outinfo</span>"; 822 if ($outmain) { 823 $output->html .= ": $outmain"; 824 $output->basic = '<tr class="r0"> 825 <th scope="row" class="cell c1 title">' . $sender->firstname . '</th> 826 <td class="cell c2 text">' . $outmain . '</td> 827 <td class="cell c3">' . $message->strtime . '</td> 828 </tr>'; 829 } else { 830 $output->basic = '<tr class="r1"> 831 <th scope="row" class="cell c1 title"></th> 832 <td class="cell c2 text">' . $outinfobasic . '</td> 833 <td class="cell c3">' . $message->strtime . '</td> 834 </tr>'; 835 } 836 $output->html .= "</td></tr></table>"; 837 return $output; 838 } 839 840 /** 841 * Given a message object this function formats it appropriately into text and html then returns the formatted data 842 * @global object 843 * @param object $message 844 * @param int $courseid 845 * @param object $currentuser 846 * @param string $chatlastrow 847 * @return bool|string Returns HTML or false 848 */ 849 function chat_format_message($message, $courseid, $currentuser, $chatlastrow=null) { 850 global $DB; 851 852 static $users; // Cache user lookups. 853 854 if (isset($users[$message->userid])) { 855 $user = $users[$message->userid]; 856 } else if ($user = $DB->get_record('user', array('id' => $message->userid), user_picture::fields())) { 857 $users[$message->userid] = $user; 858 } else { 859 return null; 860 } 861 return chat_format_message_manually($message, $courseid, $user, $currentuser, $chatlastrow); 862 } 863 864 /** 865 * @global object 866 * @param object $message message to be displayed. 867 * @param mixed $chatuser user chat data 868 * @param object $currentuser current user for whom the message should be displayed. 869 * @param int $groupingid course module grouping id 870 * @param string $theme name of the chat theme. 871 * @return bool|string Returns HTML or false 872 */ 873 function chat_format_message_theme ($message, $chatuser, $currentuser, $groupingid, $theme = 'bubble') { 874 global $CFG, $USER, $OUTPUT, $COURSE, $DB, $PAGE; 875 require_once($CFG->dirroot.'/mod/chat/locallib.php'); 876 877 static $users; // Cache user lookups. 878 879 $result = new stdClass(); 880 881 if (file_exists($CFG->dirroot . '/mod/chat/gui_ajax/theme/'.$theme.'/config.php')) { 882 include($CFG->dirroot . '/mod/chat/gui_ajax/theme/'.$theme.'/config.php'); 883 } 884 885 if (isset($users[$message->userid])) { 886 $sender = $users[$message->userid]; 887 } else if ($sender = $DB->get_record('user', array('id' => $message->userid), user_picture::fields())) { 888 $users[$message->userid] = $sender; 889 } else { 890 return null; 891 } 892 893 // Find the correct timezone for displaying this message. 894 $tz = core_date::get_user_timezone($currentuser); 895 896 if (empty($chatuser->course)) { 897 $courseid = $COURSE->id; 898 } else { 899 $courseid = $chatuser->course; 900 } 901 902 $message->strtime = userdate($message->timestamp, get_string('strftimemessage', 'chat'), $tz); 903 $message->picture = $OUTPUT->user_picture($sender, array('courseid' => $courseid)); 904 905 $message->picture = "<a target='_blank'". 906 " href=\"$CFG->wwwroot/user/view.php?id=$sender->id&course=$courseid\">$message->picture</a>"; 907 908 // Start processing the message. 909 if (!empty($message->system)) { 910 $result->type = 'system'; 911 912 $senderprofile = $CFG->wwwroot.'/user/view.php?id='.$sender->id.'&course='.$courseid; 913 $event = get_string('message'.$message->message, 'chat', fullname($sender)); 914 $eventmessage = new event_message($senderprofile, fullname($sender), $message->strtime, $event, $theme); 915 916 $output = $PAGE->get_renderer('mod_chat'); 917 $result->html = $output->render($eventmessage); 918 919 return $result; 920 } 921 922 // It's not a system event. 923 $text = trim($message->message); 924 925 // Parse the text to clean and filter it. 926 $options = new stdClass(); 927 $options->para = false; 928 $options->blanktarget = true; 929 $text = format_text($text, FORMAT_MOODLE, $options, $courseid); 930 931 // And now check for special cases. 932 $special = false; 933 $outtime = $message->strtime; 934 935 // Initialise variables. 936 $outmain = ''; 937 $patternto = '#^\s*To\s([^:]+):(.*)#'; 938 939 if (substr($text, 0, 5) == 'beep ') { 940 $special = true; 941 // It's a beep! 942 $result->type = 'beep'; 943 $beepwho = trim(substr($text, 5)); 944 945 if ($beepwho == 'all') { // Everyone. 946 $outmain = get_string('messagebeepseveryone', 'chat', fullname($sender)); 947 } else if ($beepwho == $currentuser->id) { // Current user. 948 $outmain = get_string('messagebeepsyou', 'chat', fullname($sender)); 949 } else if ($sender->id == $currentuser->id) { // Something is not caught? 950 // Allow beep for a active chat user only, else user can beep anyone and get fullname. 951 if (!empty($chatuser) && is_numeric($beepwho)) { 952 $chatusers = chat_get_users($chatuser->chatid, $chatuser->groupid, $groupingid); 953 if (array_key_exists($beepwho, $chatusers)) { 954 $outmain = get_string('messageyoubeep', 'chat', fullname($chatusers[$beepwho])); 955 } else { 956 $outmain = get_string('messageyoubeep', 'chat', $beepwho); 957 } 958 } else { 959 $outmain = get_string('messageyoubeep', 'chat', $beepwho); 960 } 961 } 962 } else if (substr($text, 0, 1) == '/') { // It's a user command. 963 $special = true; 964 $result->type = 'command'; 965 $pattern = '#(^\/)(\w+).*#'; 966 preg_match($pattern, $text, $matches); 967 $command = isset($matches[2]) ? $matches[2] : false; 968 // Support some IRC commands. 969 switch ($command) { 970 case 'me': 971 $outmain = '*** <b>'.$sender->firstname.' '.substr($text, 4).'</b>'; 972 break; 973 default: 974 // Error, we set special back to false to use the classic message output. 975 $special = false; 976 break; 977 } 978 } else if (preg_match($patternto, $text)) { 979 $special = true; 980 $result->type = 'dialogue'; 981 $matches = array(); 982 preg_match($patternto, $text, $matches); 983 if (isset($matches[1]) && isset($matches[2])) { 984 $outmain = $sender->firstname.' <b>'.get_string('saidto', 'chat').'</b> <i>'.$matches[1].'</i>: '.$matches[2]; 985 } else { 986 // Error, we set special back to false to use the classic message output. 987 $special = false; 988 } 989 } 990 991 if (!$special) { 992 $outmain = $text; 993 } 994 995 $result->text = strip_tags($outtime.': '.$outmain); 996 997 $mymessageclass = ''; 998 if ($sender->id == $USER->id) { 999 $mymessageclass = 'chat-message-mymessage'; 1000 } 1001 1002 $senderprofile = $CFG->wwwroot.'/user/view.php?id='.$sender->id.'&course='.$courseid; 1003 $usermessage = new user_message($senderprofile, fullname($sender), $message->picture, 1004 $mymessageclass, $outtime, $outmain, $theme); 1005 1006 $output = $PAGE->get_renderer('mod_chat'); 1007 $result->html = $output->render($usermessage); 1008 1009 // When user beeps other user, then don't show any timestamp to other users in chat. 1010 if (('' === $outmain) && $special) { 1011 return false; 1012 } else { 1013 return $result; 1014 } 1015 } 1016 1017 /** 1018 * @global object $DB 1019 * @global object $CFG 1020 * @global object $COURSE 1021 * @global object $OUTPUT 1022 * @param object $users 1023 * @param object $course 1024 * @return array return formatted user list 1025 */ 1026 function chat_format_userlist($users, $course) { 1027 global $CFG, $DB, $COURSE, $OUTPUT; 1028 $result = array(); 1029 foreach ($users as $user) { 1030 $item = array(); 1031 $item['name'] = fullname($user); 1032 $item['url'] = $CFG->wwwroot.'/user/view.php?id='.$user->id.'&course='.$course->id; 1033 $item['picture'] = $OUTPUT->user_picture($user); 1034 $item['id'] = $user->id; 1035 $result[] = $item; 1036 } 1037 return $result; 1038 } 1039 1040 /** 1041 * Print json format error 1042 * @param string $level 1043 * @param string $msg 1044 */ 1045 function chat_print_error($level, $msg) { 1046 header('Content-Length: ' . ob_get_length() ); 1047 $error = new stdClass(); 1048 $error->level = $level; 1049 $error->msg = $msg; 1050 $response['error'] = $error; 1051 echo json_encode($response); 1052 ob_end_flush(); 1053 exit; 1054 } 1055 1056 /** 1057 * List the actions that correspond to a view of this module. 1058 * This is used by the participation report. 1059 * 1060 * Note: This is not used by new logging system. Event with 1061 * crud = 'r' and edulevel = LEVEL_PARTICIPATING will 1062 * be considered as view action. 1063 * 1064 * @return array 1065 */ 1066 function chat_get_view_actions() { 1067 return array('view', 'view all', 'report'); 1068 } 1069 1070 /** 1071 * List the actions that correspond to a post of this module. 1072 * This is used by the participation report. 1073 * 1074 * Note: This is not used by new logging system. Event with 1075 * crud = ('c' || 'u' || 'd') and edulevel = LEVEL_PARTICIPATING 1076 * will be considered as post action. 1077 * 1078 * @return array 1079 */ 1080 function chat_get_post_actions() { 1081 return array('talk'); 1082 } 1083 1084 /** 1085 * @global object 1086 * @global object 1087 * @param array $courses 1088 * @param array $htmlarray Passed by reference 1089 */ 1090 function chat_print_overview($courses, &$htmlarray) { 1091 global $USER, $CFG; 1092 1093 if (empty($courses) || !is_array($courses) || count($courses) == 0) { 1094 return array(); 1095 } 1096 1097 if (!$chats = get_all_instances_in_courses('chat', $courses)) { 1098 return; 1099 } 1100 1101 $strchat = get_string('modulename', 'chat'); 1102 $strnextsession = get_string('nextsession', 'chat'); 1103 1104 foreach ($chats as $chat) { 1105 if ($chat->chattime and $chat->schedule) { // A chat is scheduled. 1106 $str = '<div class="chat overview"><div class="name">'. 1107 $strchat.': <a '.($chat->visible ? '' : ' class="dimmed"'). 1108 ' href="'.$CFG->wwwroot.'/mod/chat/view.php?id='.$chat->coursemodule.'">'. 1109 $chat->name.'</a></div>'; 1110 $str .= '<div class="info">'.$strnextsession.': '.userdate($chat->chattime).'</div></div>'; 1111 1112 if (empty($htmlarray[$chat->course]['chat'])) { 1113 $htmlarray[$chat->course]['chat'] = $str; 1114 } else { 1115 $htmlarray[$chat->course]['chat'] .= $str; 1116 } 1117 } 1118 } 1119 } 1120 1121 1122 /** 1123 * Implementation of the function for printing the form elements that control 1124 * whether the course reset functionality affects the chat. 1125 * 1126 * @param object $mform form passed by reference 1127 */ 1128 function chat_reset_course_form_definition(&$mform) { 1129 $mform->addElement('header', 'chatheader', get_string('modulenameplural', 'chat')); 1130 $mform->addElement('advcheckbox', 'reset_chat', get_string('removemessages', 'chat')); 1131 } 1132 1133 /** 1134 * Course reset form defaults. 1135 * 1136 * @param object $course 1137 * @return array 1138 */ 1139 function chat_reset_course_form_defaults($course) { 1140 return array('reset_chat' => 1); 1141 } 1142 1143 /** 1144 * Actual implementation of the reset course functionality, delete all the 1145 * chat messages for course $data->courseid. 1146 * 1147 * @global object 1148 * @global object 1149 * @param object $data the data submitted from the reset course. 1150 * @return array status array 1151 */ 1152 function chat_reset_userdata($data) { 1153 global $CFG, $DB; 1154 1155 $componentstr = get_string('modulenameplural', 'chat'); 1156 $status = array(); 1157 1158 if (!empty($data->reset_chat)) { 1159 $chatessql = "SELECT ch.id 1160 FROM {chat} ch 1161 WHERE ch.course=?"; 1162 $params = array($data->courseid); 1163 1164 $DB->delete_records_select('chat_messages', "chatid IN ($chatessql)", $params); 1165 $DB->delete_records_select('chat_messages_current', "chatid IN ($chatessql)", $params); 1166 $DB->delete_records_select('chat_users', "chatid IN ($chatessql)", $params); 1167 $status[] = array('component' => $componentstr, 'item' => get_string('removemessages', 'chat'), 'error' => false); 1168 } 1169 1170 // Updating dates - shift may be negative too. 1171 if ($data->timeshift) { 1172 shift_course_mod_dates('chat', array('chattime'), $data->timeshift, $data->courseid); 1173 $status[] = array('component' => $componentstr, 'item' => get_string('datechanged'), 'error' => false); 1174 } 1175 1176 return $status; 1177 } 1178 1179 /** 1180 * Returns all other caps used in module 1181 * 1182 * @return array 1183 */ 1184 function chat_get_extra_capabilities() { 1185 return array('moodle/site:accessallgroups', 'moodle/site:viewfullnames'); 1186 } 1187 1188 1189 /** 1190 * @param string $feature FEATURE_xx constant for requested feature 1191 * @return mixed True if module supports feature, null if doesn't know 1192 */ 1193 function chat_supports($feature) { 1194 switch($feature) { 1195 case FEATURE_GROUPS: 1196 return true; 1197 case FEATURE_GROUPINGS: 1198 return true; 1199 case FEATURE_MOD_INTRO: 1200 return true; 1201 case FEATURE_BACKUP_MOODLE2: 1202 return true; 1203 case FEATURE_COMPLETION_TRACKS_VIEWS: 1204 return true; 1205 case FEATURE_GRADE_HAS_GRADE: 1206 return false; 1207 case FEATURE_GRADE_OUTCOMES: 1208 return true; 1209 case FEATURE_SHOW_DESCRIPTION: 1210 return true; 1211 default: 1212 return null; 1213 } 1214 } 1215 1216 function chat_extend_navigation($navigation, $course, $module, $cm) { 1217 global $CFG; 1218 1219 $currentgroup = groups_get_activity_group($cm, true); 1220 1221 if (has_capability('mod/chat:chat', context_module::instance($cm->id))) { 1222 $strenterchat = get_string('enterchat', 'chat'); 1223 1224 $target = $CFG->wwwroot.'/mod/chat/'; 1225 $params = array('id' => $cm->instance); 1226 1227 if ($currentgroup) { 1228 $params['groupid'] = $currentgroup; 1229 } 1230 1231 $links = array(); 1232 1233 $url = new moodle_url($target.'gui_'.$CFG->chat_method.'/index.php', $params); 1234 $action = new popup_action('click', $url, 'chat'.$course->id.$cm->instance.$currentgroup, 1235 array('height' => 500, 'width' => 700)); 1236 $links[] = new action_link($url, $strenterchat, $action); 1237 1238 $url = new moodle_url($target.'gui_basic/index.php', $params); 1239 $action = new popup_action('click', $url, 'chat'.$course->id.$cm->instance.$currentgroup, 1240 array('height' => 500, 'width' => 700)); 1241 $links[] = new action_link($url, get_string('noframesjs', 'message'), $action); 1242 1243 foreach ($links as $link) { 1244 $navigation->add($link->text, $link, navigation_node::TYPE_SETTING, null , null, new pix_icon('i/group' , '')); 1245 } 1246 } 1247 1248 $chatusers = chat_get_users($cm->instance, $currentgroup, $cm->groupingid); 1249 if (is_array($chatusers) && count($chatusers) > 0) { 1250 $users = $navigation->add(get_string('currentusers', 'chat')); 1251 foreach ($chatusers as $chatuser) { 1252 $userlink = new moodle_url('/user/view.php', array('id' => $chatuser->id, 'course' => $course->id)); 1253 $users->add(fullname($chatuser).' '.format_time(time() - $chatuser->lastmessageping), 1254 $userlink, navigation_node::TYPE_USER, null, null, new pix_icon('i/user', '')); 1255 } 1256 } 1257 } 1258 1259 /** 1260 * Adds module specific settings to the settings block 1261 * 1262 * @param settings_navigation $settings The settings navigation object 1263 * @param navigation_node $chatnode The node to add module settings to 1264 */ 1265 function chat_extend_settings_navigation(settings_navigation $settings, navigation_node $chatnode) { 1266 global $DB, $PAGE, $USER; 1267 $chat = $DB->get_record("chat", array("id" => $PAGE->cm->instance)); 1268 1269 if ($chat->chattime && $chat->schedule) { 1270 $nextsessionnode = $chatnode->add(get_string('nextsession', 'chat'). 1271 ': '.userdate($chat->chattime). 1272 ' ('.usertimezone($USER->timezone).')'); 1273 $nextsessionnode->add_class('note'); 1274 } 1275 1276 $currentgroup = groups_get_activity_group($PAGE->cm, true); 1277 if ($currentgroup) { 1278 $groupselect = " AND groupid = '$currentgroup'"; 1279 } else { 1280 $groupselect = ''; 1281 } 1282 1283 if ($chat->studentlogs || has_capability('mod/chat:readlog', $PAGE->cm->context)) { 1284 if ($DB->get_records_select('chat_messages', "chatid = ? $groupselect", array($chat->id))) { 1285 $chatnode->add(get_string('viewreport', 'chat'), new moodle_url('/mod/chat/report.php', array('id' => $PAGE->cm->id))); 1286 } 1287 } 1288 } 1289 1290 /** 1291 * user logout event handler 1292 * 1293 * @param \core\event\user_loggedout $event The event. 1294 * @return void 1295 */ 1296 function chat_user_logout(\core\event\user_loggedout $event) { 1297 global $DB; 1298 $DB->delete_records('chat_users', array('userid' => $event->objectid)); 1299 } 1300 1301 /** 1302 * Return a list of page types 1303 * @param string $pagetype current page type 1304 * @param stdClass $parentcontext Block's parent context 1305 * @param stdClass $currentcontext Current context of block 1306 */ 1307 function chat_page_type_list($pagetype, $parentcontext, $currentcontext) { 1308 $modulepagetype = array('mod-chat-*' => get_string('page-mod-chat-x', 'chat')); 1309 return $modulepagetype; 1310 } 1311 1312 /** 1313 * Return a list of the latest messages in the given chat session. 1314 * 1315 * @param stdClass $chatuser chat user session data 1316 * @param int $chatlasttime last time messages were retrieved 1317 * @return array list of messages 1318 * @since Moodle 3.0 1319 */ 1320 function chat_get_latest_messages($chatuser, $chatlasttime) { 1321 global $DB; 1322 1323 $params = array('groupid' => $chatuser->groupid, 'chatid' => $chatuser->chatid, 'lasttime' => $chatlasttime); 1324 1325 $groupselect = $chatuser->groupid ? " AND (groupid=" . $chatuser->groupid . " OR groupid=0) " : ""; 1326 1327 return $DB->get_records_select('chat_messages_current', 'chatid = :chatid AND timestamp > :lasttime ' . $groupselect, 1328 $params, 'timestamp ASC'); 1329 } 1330 1331 /** 1332 * Mark the activity completed (if required) and trigger the course_module_viewed event. 1333 * 1334 * @param stdClass $chat chat object 1335 * @param stdClass $course course object 1336 * @param stdClass $cm course module object 1337 * @param stdClass $context context object 1338 * @since Moodle 3.0 1339 */ 1340 function chat_view($chat, $course, $cm, $context) { 1341 1342 // Trigger course_module_viewed event. 1343 $params = array( 1344 'context' => $context, 1345 'objectid' => $chat->id 1346 ); 1347 1348 $event = \mod_chat\event\course_module_viewed::create($params); 1349 $event->add_record_snapshot('course_modules', $cm); 1350 $event->add_record_snapshot('course', $course); 1351 $event->add_record_snapshot('chat', $chat); 1352 $event->trigger(); 1353 1354 // Completion. 1355 $completion = new completion_info($course); 1356 $completion->set_module_viewed($cm); 1357 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Aug 11 10:00:09 2016 | Cross-referenced by PHPXref 0.7.1 |