MDL-55252 mod_chat: prevent direct script access to lib files.
[moodle.git] / mod / chat / lib.php
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/>.
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  */
25 defined('MOODLE_INTERNAL') || die();
27 require_once($CFG->dirroot.'/calendar/lib.php');
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);
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);
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>";
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>";
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;
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);
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 }
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;
112     $chat->timemodified = time();
114     $returnid = $DB->insert_record("chat", $chat);
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;
128     calendar_event::create($event);
130     return $returnid;
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;
145     $chat->timemodified = time();
146     $chat->id = $chat->instance;
148     $DB->update_record("chat", $chat);
150     $event = new stdClass();
152     if ($event->id = $DB->get_field('event', 'id', array('modulename' => 'chat', 'instance' => $chat->id))) {
154         $event->name        = $chat->name;
155         $event->description = format_module_intro('chat', $chat, $chat->coursemodule);
156         $event->timestart   = $chat->chattime;
158         $calendarevent = calendar_event::load($event->id);
159         $calendarevent->update($event);
160     }
162     return true;
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;
177     if (! $chat = $DB->get_record('chat', array('id' => $id))) {
178         return false;
179     }
181     $result = true;
183     // Delete any dependent records here.
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     }
198     if (! $DB->delete_records('event', array('modulename' => 'chat', 'instance' => $chat->id))) {
199         $result = false;
200     }
202     return $result;
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;
220     // This is approximate only, but it is really fast.
221     $timeout = $CFG->chat_old_ping * 10;
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     }
234     $past     = array();
235     $current  = array();
236     $modinfo = get_fast_modinfo($course); // Reference needed because we might load the groups.
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         }
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             }
255             continue;
256         }
258         // Verify groups in separate mode.
259         if (!$mygroupids = $modinfo->get_groups($cm->groupingid)) {
260             continue;
261         }
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);
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         }
277         $mcms[$cmid]->lasttime = $mcm->lasttime;
278         if ($timeout > time() - $mcm->lasttime) {
279             $current[] = $cm;
280         } else {
281             $past[] = $cm;
282         }
283     }
285     if (!$past and !$current) {
286         return false;
287     }
289     $strftimerecent = get_string('strftimerecent');
291     if ($past) {
292         echo $OUTPUT->heading(get_string("pastchats", 'chat').':', 3);
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     }
302     if ($current) {
303         echo $OUTPUT->heading(get_string("currentchats", 'chat').':', 3);
305         $oldest = floor((time() - $CFG->chat_old_ping) / 10) * 10;  // Better db caching.
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.
312         $params = array('timeold' => $timeold, 'timeoldext' => $timeoldext, 'cmid' => $cm->id);
314         $timeout = "AND ((chu.version<>'basic' AND chu.lastping>:timeold) OR (chu.version='basic' AND chu.lastping>:timeoldext))";
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             }
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             }
337             $link = $CFG->wwwroot.'/mod/chat/view.php?id='.$cm->id;
338             $date = userdate($mcms[$cm->id]->lasttime, $strftimerecent);
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     }
354     return true;
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;
368     chat_update_chat_times();
370     chat_delete_old_users();
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";
377     $sql = "DELETE
378               FROM {chat_messages}
379              WHERE ($subselect) > 0 AND timestamp < ( ".time()." -($subselect) * 24 * 3600)";
381     $DB->execute($sql);
383     $sql = "DELETE
384               FROM {chat_messages_current}
385              WHERE timestamp < ( ".time()." - 8 * 3600)";
387     $DB->execute($sql);
389     return true;
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;
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'));
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;
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));
437             calendar_event::create($event);
438         }
439     }
440     return true;
443 // Functions that require some SQL.
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;
455     $params = array('chatid' => $chatid, 'groupid' => $groupid, 'groupingid' => $groupingid);
457     if ($groupid) {
458         $groupselect = " AND (c.groupid=:groupid OR c.groupid='0')";
459     } else {
460         $groupselect = "";
461     }
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 ";
467     } else {
468         $groupingjoin = '';
469     }
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);
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;
488     $params = array('chatid' => $chatid, 'groupid' => $groupid);
490     if ($groupid) {
491         $groupselect = "AND (groupid=:groupid OR groupid=0)";
492     } else {
493         $groupselect = "";
494     }
496     $sql = "SELECT *
497         FROM {chat_messages_current} WHERE chatid = :chatid $groupselect
498         ORDER BY timestamp DESC";
500     // Return the lastest one message.
501     return $DB->get_record_sql($sql, $params, true);
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;
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();
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         }
533         if (($chatuser->course != $course->id) or ($chatuser->userid != $USER->id)) {
534             return false;
535         }
536         $DB->update_record('chat_users', $chatuser);
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.
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         }
556         $DB->insert_record('chat_users', $chatuser);
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     }
565     return $chatuser->sid;
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;
578     $timeold = time() - $CFG->chat_old_ping;
579     $timeoldext = time() - ($CFG->chat_old_ping * 10); // JSless gui_basic needs much longer timeouts.
581     $query = "(version<>'basic' AND lastping<?) OR (version='basic' AND lastping<?)";
582     $params = array($timeold, $timeoldext);
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     }
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;
603     $timenow = time();
605     $params = array('timenow' => $timenow, 'chatid' => $chatid);
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     }
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);
636         $event = new stdClass(); // Update calendar too.
638         $cond = "modulename='chat' AND instance = :chatid AND timestart <> :chattime";
639         $params = array('chattime' => $chat->chattime, 'chatid' => $chat->id);
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     }
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;
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();
670     $messageid = $DB->insert_record('chat_messages', $message);
671     $DB->insert_record('chat_messages_current', $message);
672     $message->id = $messageid;
674     if (!$system) {
676         if (empty($cm)) {
677             $cm = get_coursemodule_from_instance('chat', $chatuser->chatid, $chatuser->course);
678         }
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     }
691     return $message->id;
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;
707     $output = new stdClass();
708     $output->beep = false;       // By default.
709     $output->refreshusers = false; // By default.
711     // Find the correct timezone for displaying this message.
712     $tz = core_date::get_user_timezone($currentuser);
714     $message->strtime = userdate($message->timestamp, get_string('strftimemessage', 'chat'), $tz);
716     $message->picture = $OUTPUT->user_picture($sender, array('size' => false, 'courseid' => $courseid, 'link' => false));
718     if ($courseid) {
719         $message->picture = "<a onclick=\"window.open('$CFG->wwwroot/user/view.php?id=$sender->id&amp;course=$courseid')\"".
720                             " href=\"$CFG->wwwroot/user/view.php?id=$sender->id&amp;course=$courseid\">$message->picture</a>";
721     }
723     // Calculate the row class.
724     if ($chatlastrow !== null) {
725         $rowclass = ' class="r'.$chatlastrow.'" ';
726     } else {
727         $rowclass = '';
728     }
730     // Start processing the message.
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     }
748     // It's not a system event.
749     $text = trim($message->message);
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);
757     // And now check for special cases.
758     $patternto = '#^\s*To\s([^:]+):(.*)#';
759     $special = false;
761     if (substr($text, 0, 5) == 'beep ') {
762         // It's a beep!
763         $special = true;
764         $beepwho = trim(substr($text, 5));
766         if ($beepwho == 'all') {   // Everyone.
767             $outinfobasic = get_string('messagebeepseveryone', 'chat', fullname($sender));
768             $outinfo = $message->strtime . ': ' . $outinfobasic;
769             $outmain = '';
771             $output->beep = true;  // Eventually this should be set to a filename uploaded by the user.
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;
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     }
811     if (!$special) {
812         $outinfo = $message->strtime.' '.$sender->firstname;
813         $outmain = $text;
814     }
816     // Format the message as a small table.
818     $output->text  = strip_tags($outinfo.': '.$outmain);
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;
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;
852     static $users;     // Cache user lookups.
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);
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');
877     static $users;     // Cache user lookups.
879     $result = new stdClass();
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     }
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     }
893     // Find the correct timezone for displaying this message.
894     $tz = core_date::get_user_timezone($currentuser);
896     if (empty($chatuser->course)) {
897         $courseid = $COURSE->id;
898     } else {
899         $courseid = $chatuser->course;
900     }
902     $message->strtime = userdate($message->timestamp, get_string('strftimemessage', 'chat'), $tz);
903     $message->picture = $OUTPUT->user_picture($sender, array('courseid' => $courseid));
905     $message->picture = "<a target='_blank'".
906                         " href=\"$CFG->wwwroot/user/view.php?id=$sender->id&amp;course=$courseid\">$message->picture</a>";
908     // Start processing the message.
909     if (!empty($message->system)) {
910         $result->type = 'system';
912         $senderprofile = $CFG->wwwroot.'/user/view.php?id='.$sender->id.'&amp;course='.$courseid;
913         $event = get_string('message'.$message->message, 'chat', fullname($sender));
914         $eventmessage = new event_message($senderprofile, fullname($sender), $message->strtime, $event, $theme);
916         $output = $PAGE->get_renderer('mod_chat');
917         $result->html = $output->render($eventmessage);
919         return $result;
920     }
922     // It's not a system event.
923     $text = trim($message->message);
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);
931     // And now check for special cases.
932     $special = false;
933     $outtime = $message->strtime;
935     // Initialise variables.
936     $outmain = '';
937     $patternto = '#^\s*To\s([^:]+):(.*)#';
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));
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     }
991     if (!$special) {
992         $outmain = $text;
993     }
995     $result->text = strip_tags($outtime.': '.$outmain);
997     $mymessageclass = '';
998     if ($sender->id == $USER->id) {
999         $mymessageclass = 'chat-message-mymessage';
1000     }
1002     $senderprofile = $CFG->wwwroot.'/user/view.php?id='.$sender->id.'&amp;course='.$courseid;
1003     $usermessage = new user_message($senderprofile, fullname($sender), $message->picture,
1004                                     $mymessageclass, $outtime, $outmain, $theme);
1006     $output = $PAGE->get_renderer('mod_chat');
1007     $result->html = $output->render($usermessage);
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     }
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.'&amp;course='.$course->id;
1033         $item['picture'] = $OUTPUT->user_picture($user);
1034         $item['id'] = $user->id;
1035         $result[] = $item;
1036     }
1037     return $result;
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;
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');
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');
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;
1093     if (empty($courses) || !is_array($courses) || count($courses) == 0) {
1094         return array();
1095     }
1097     if (!$chats = get_all_instances_in_courses('chat', $courses)) {
1098         return;
1099     }
1101     $strchat = get_string('modulename', 'chat');
1102     $strnextsession  = get_string('nextsession', 'chat');
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>';
1112             if (empty($htmlarray[$chat->course]['chat'])) {
1113                 $htmlarray[$chat->course]['chat'] = $str;
1114             } else {
1115                 $htmlarray[$chat->course]['chat'] .= $str;
1116             }
1117         }
1118     }
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'));
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);
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;
1155     $componentstr = get_string('modulenameplural', 'chat');
1156     $status = array();
1158     if (!empty($data->reset_chat)) {
1159         $chatessql = "SELECT ch.id
1160                         FROM {chat} ch
1161                        WHERE ch.course=?";
1162         $params = array($data->courseid);
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     }
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     }
1176     return $status;
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');
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     }
1216 function chat_extend_navigation($navigation, $course, $module, $cm) {
1217     global $CFG;
1219     $currentgroup = groups_get_activity_group($cm, true);
1221     if (has_capability('mod/chat:chat', context_module::instance($cm->id))) {
1222         $strenterchat    = get_string('enterchat', 'chat');
1224         $target = $CFG->wwwroot.'/mod/chat/';
1225         $params = array('id' => $cm->instance);
1227         if ($currentgroup) {
1228             $params['groupid'] = $currentgroup;
1229         }
1231         $links = array();
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);
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);
1243         foreach ($links as $link) {
1244             $navigation->add($link->text, $link, navigation_node::TYPE_SETTING, null , null, new pix_icon('i/group' , ''));
1245         }
1246     }
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     }
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));
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     }
1276     $currentgroup = groups_get_activity_group($PAGE->cm, true);
1277     if ($currentgroup) {
1278         $groupselect = " AND groupid = '$currentgroup'";
1279     } else {
1280         $groupselect = '';
1281     }
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     }
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));
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;
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;
1323     $params = array('groupid' => $chatuser->groupid, 'chatid' => $chatuser->chatid, 'lasttime' => $chatlasttime);
1325     $groupselect = $chatuser->groupid ? " AND (groupid=" . $chatuser->groupid . " OR groupid=0) " : "";
1327     return $DB->get_records_select('chat_messages_current', 'chatid = :chatid AND timestamp > :lasttime ' . $groupselect,
1328                                     $params, 'timestamp ASC');
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) {
1342     // Trigger course_module_viewed event.
1343     $params = array(
1344         'context' => $context,
1345         'objectid' => $chat->id
1346     );
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();
1354     // Completion.
1355     $completion = new completion_info($course);
1356     $completion->set_module_viewed($cm);