PageRenderTime 45ms CodeModel.GetById 28ms RepoModel.GetById 0ms app.codeStats 1ms

/mod/forum/lib.php

https://bitbucket.org/moodle/moodle
PHP | 7037 lines | 6600 code | 185 blank | 252 comment | 133 complexity | 4eae533791952e05c0740e5a66b1c09d MD5 | raw file
Possible License(s): Apache-2.0, LGPL-2.1, BSD-3-Clause, MIT, GPL-3.0
  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. * @package mod_forum
  18. * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
  19. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  20. */
  21. defined('MOODLE_INTERNAL') || die();
  22. /** Include required files */
  23. require_once(__DIR__ . '/deprecatedlib.php');
  24. require_once($CFG->libdir.'/filelib.php');
  25. /// CONSTANTS ///////////////////////////////////////////////////////////
  26. define('FORUM_MODE_FLATOLDEST', 1);
  27. define('FORUM_MODE_FLATNEWEST', -1);
  28. define('FORUM_MODE_THREADED', 2);
  29. define('FORUM_MODE_NESTED', 3);
  30. define('FORUM_MODE_NESTED_V2', 4);
  31. define('FORUM_CHOOSESUBSCRIBE', 0);
  32. define('FORUM_FORCESUBSCRIBE', 1);
  33. define('FORUM_INITIALSUBSCRIBE', 2);
  34. define('FORUM_DISALLOWSUBSCRIBE',3);
  35. /**
  36. * FORUM_TRACKING_OFF - Tracking is not available for this forum.
  37. */
  38. define('FORUM_TRACKING_OFF', 0);
  39. /**
  40. * FORUM_TRACKING_OPTIONAL - Tracking is based on user preference.
  41. */
  42. define('FORUM_TRACKING_OPTIONAL', 1);
  43. /**
  44. * FORUM_TRACKING_FORCED - Tracking is on, regardless of user setting.
  45. * Treated as FORUM_TRACKING_OPTIONAL if $CFG->forum_allowforcedreadtracking is off.
  46. */
  47. define('FORUM_TRACKING_FORCED', 2);
  48. define('FORUM_MAILED_PENDING', 0);
  49. define('FORUM_MAILED_SUCCESS', 1);
  50. define('FORUM_MAILED_ERROR', 2);
  51. if (!defined('FORUM_CRON_USER_CACHE')) {
  52. /** Defines how many full user records are cached in forum cron. */
  53. define('FORUM_CRON_USER_CACHE', 5000);
  54. }
  55. /**
  56. * FORUM_POSTS_ALL_USER_GROUPS - All the posts in groups where the user is enrolled.
  57. */
  58. define('FORUM_POSTS_ALL_USER_GROUPS', -2);
  59. define('FORUM_DISCUSSION_PINNED', 1);
  60. define('FORUM_DISCUSSION_UNPINNED', 0);
  61. /// STANDARD FUNCTIONS ///////////////////////////////////////////////////////////
  62. /**
  63. * Given an object containing all the necessary data,
  64. * (defined by the form in mod_form.php) this function
  65. * will create a new instance and return the id number
  66. * of the new instance.
  67. *
  68. * @param stdClass $forum add forum instance
  69. * @param mod_forum_mod_form $mform
  70. * @return int intance id
  71. */
  72. function forum_add_instance($forum, $mform = null) {
  73. global $CFG, $DB;
  74. require_once($CFG->dirroot.'/mod/forum/locallib.php');
  75. $forum->timemodified = time();
  76. if (empty($forum->assessed)) {
  77. $forum->assessed = 0;
  78. }
  79. if (empty($forum->ratingtime) or empty($forum->assessed)) {
  80. $forum->assesstimestart = 0;
  81. $forum->assesstimefinish = 0;
  82. }
  83. $forum->id = $DB->insert_record('forum', $forum);
  84. $modcontext = context_module::instance($forum->coursemodule);
  85. if ($forum->type == 'single') { // Create related discussion.
  86. $discussion = new stdClass();
  87. $discussion->course = $forum->course;
  88. $discussion->forum = $forum->id;
  89. $discussion->name = $forum->name;
  90. $discussion->assessed = $forum->assessed;
  91. $discussion->message = $forum->intro;
  92. $discussion->messageformat = $forum->introformat;
  93. $discussion->messagetrust = trusttext_trusted(context_course::instance($forum->course));
  94. $discussion->mailnow = false;
  95. $discussion->groupid = -1;
  96. $message = '';
  97. $discussion->id = forum_add_discussion($discussion, null, $message);
  98. if ($mform and $draftid = file_get_submitted_draft_itemid('introeditor')) {
  99. // Ugly hack - we need to copy the files somehow.
  100. $discussion = $DB->get_record('forum_discussions', array('id'=>$discussion->id), '*', MUST_EXIST);
  101. $post = $DB->get_record('forum_posts', array('id'=>$discussion->firstpost), '*', MUST_EXIST);
  102. $options = array('subdirs'=>true); // Use the same options as intro field!
  103. $post->message = file_save_draft_area_files($draftid, $modcontext->id, 'mod_forum', 'post', $post->id, $options, $post->message);
  104. $DB->set_field('forum_posts', 'message', $post->message, array('id'=>$post->id));
  105. }
  106. }
  107. forum_update_calendar($forum, $forum->coursemodule);
  108. forum_grade_item_update($forum);
  109. $completiontimeexpected = !empty($forum->completionexpected) ? $forum->completionexpected : null;
  110. \core_completion\api::update_completion_date_event($forum->coursemodule, 'forum', $forum->id, $completiontimeexpected);
  111. return $forum->id;
  112. }
  113. /**
  114. * Handle changes following the creation of a forum instance.
  115. * This function is typically called by the course_module_created observer.
  116. *
  117. * @param object $context the forum context
  118. * @param stdClass $forum The forum object
  119. * @return void
  120. */
  121. function forum_instance_created($context, $forum) {
  122. if ($forum->forcesubscribe == FORUM_INITIALSUBSCRIBE) {
  123. $users = \mod_forum\subscriptions::get_potential_subscribers($context, 0, 'u.id, u.email');
  124. foreach ($users as $user) {
  125. \mod_forum\subscriptions::subscribe_user($user->id, $forum, $context);
  126. }
  127. }
  128. }
  129. /**
  130. * Given an object containing all the necessary data,
  131. * (defined by the form in mod_form.php) this function
  132. * will update an existing instance with new data.
  133. *
  134. * @global object
  135. * @param object $forum forum instance (with magic quotes)
  136. * @return bool success
  137. */
  138. function forum_update_instance($forum, $mform) {
  139. global $CFG, $DB, $OUTPUT, $USER;
  140. require_once($CFG->dirroot.'/mod/forum/locallib.php');
  141. $forum->timemodified = time();
  142. $forum->id = $forum->instance;
  143. if (empty($forum->assessed)) {
  144. $forum->assessed = 0;
  145. }
  146. if (empty($forum->ratingtime) or empty($forum->assessed)) {
  147. $forum->assesstimestart = 0;
  148. $forum->assesstimefinish = 0;
  149. }
  150. $oldforum = $DB->get_record('forum', array('id'=>$forum->id));
  151. // MDL-3942 - if the aggregation type or scale (i.e. max grade) changes then recalculate the grades for the entire forum
  152. // if scale changes - do we need to recheck the ratings, if ratings higher than scale how do we want to respond?
  153. // for count and sum aggregation types the grade we check to make sure they do not exceed the scale (i.e. max score) when calculating the grade
  154. $updategrades = false;
  155. if ($oldforum->assessed <> $forum->assessed) {
  156. // Whether this forum is rated.
  157. $updategrades = true;
  158. }
  159. if ($oldforum->scale <> $forum->scale) {
  160. // The scale currently in use.
  161. $updategrades = true;
  162. }
  163. if (empty($oldforum->grade_forum) || $oldforum->grade_forum <> $forum->grade_forum) {
  164. // The whole forum grading.
  165. $updategrades = true;
  166. }
  167. if ($updategrades) {
  168. forum_update_grades($forum); // Recalculate grades for the forum.
  169. }
  170. if ($forum->type == 'single') { // Update related discussion and post.
  171. $discussions = $DB->get_records('forum_discussions', array('forum'=>$forum->id), 'timemodified ASC');
  172. if (!empty($discussions)) {
  173. if (count($discussions) > 1) {
  174. echo $OUTPUT->notification(get_string('warnformorepost', 'forum'));
  175. }
  176. $discussion = array_pop($discussions);
  177. } else {
  178. // try to recover by creating initial discussion - MDL-16262
  179. $discussion = new stdClass();
  180. $discussion->course = $forum->course;
  181. $discussion->forum = $forum->id;
  182. $discussion->name = $forum->name;
  183. $discussion->assessed = $forum->assessed;
  184. $discussion->message = $forum->intro;
  185. $discussion->messageformat = $forum->introformat;
  186. $discussion->messagetrust = true;
  187. $discussion->mailnow = false;
  188. $discussion->groupid = -1;
  189. $message = '';
  190. forum_add_discussion($discussion, null, $message);
  191. if (! $discussion = $DB->get_record('forum_discussions', array('forum'=>$forum->id))) {
  192. print_error('cannotadd', 'forum');
  193. }
  194. }
  195. if (! $post = $DB->get_record('forum_posts', array('id'=>$discussion->firstpost))) {
  196. print_error('cannotfindfirstpost', 'forum');
  197. }
  198. $cm = get_coursemodule_from_instance('forum', $forum->id);
  199. $modcontext = context_module::instance($cm->id, MUST_EXIST);
  200. $post = $DB->get_record('forum_posts', array('id'=>$discussion->firstpost), '*', MUST_EXIST);
  201. $post->subject = $forum->name;
  202. $post->message = $forum->intro;
  203. $post->messageformat = $forum->introformat;
  204. $post->messagetrust = trusttext_trusted($modcontext);
  205. $post->modified = $forum->timemodified;
  206. $post->userid = $USER->id; // MDL-18599, so that current teacher can take ownership of activities.
  207. if ($mform and $draftid = file_get_submitted_draft_itemid('introeditor')) {
  208. // Ugly hack - we need to copy the files somehow.
  209. $options = array('subdirs'=>true); // Use the same options as intro field!
  210. $post->message = file_save_draft_area_files($draftid, $modcontext->id, 'mod_forum', 'post', $post->id, $options, $post->message);
  211. }
  212. \mod_forum\local\entities\post::add_message_counts($post);
  213. $DB->update_record('forum_posts', $post);
  214. $discussion->name = $forum->name;
  215. $DB->update_record('forum_discussions', $discussion);
  216. }
  217. $DB->update_record('forum', $forum);
  218. $modcontext = context_module::instance($forum->coursemodule);
  219. if (($forum->forcesubscribe == FORUM_INITIALSUBSCRIBE) && ($oldforum->forcesubscribe <> $forum->forcesubscribe)) {
  220. $users = \mod_forum\subscriptions::get_potential_subscribers($modcontext, 0, 'u.id, u.email', '');
  221. foreach ($users as $user) {
  222. \mod_forum\subscriptions::subscribe_user($user->id, $forum, $modcontext);
  223. }
  224. }
  225. forum_update_calendar($forum, $forum->coursemodule);
  226. forum_grade_item_update($forum);
  227. $completiontimeexpected = !empty($forum->completionexpected) ? $forum->completionexpected : null;
  228. \core_completion\api::update_completion_date_event($forum->coursemodule, 'forum', $forum->id, $completiontimeexpected);
  229. return true;
  230. }
  231. /**
  232. * Given an ID of an instance of this module,
  233. * this function will permanently delete the instance
  234. * and any data that depends on it.
  235. *
  236. * @global object
  237. * @param int $id forum instance id
  238. * @return bool success
  239. */
  240. function forum_delete_instance($id) {
  241. global $DB;
  242. if (!$forum = $DB->get_record('forum', array('id'=>$id))) {
  243. return false;
  244. }
  245. if (!$cm = get_coursemodule_from_instance('forum', $forum->id)) {
  246. return false;
  247. }
  248. if (!$course = $DB->get_record('course', array('id'=>$cm->course))) {
  249. return false;
  250. }
  251. $context = context_module::instance($cm->id);
  252. // now get rid of all files
  253. $fs = get_file_storage();
  254. $fs->delete_area_files($context->id);
  255. $result = true;
  256. \core_completion\api::update_completion_date_event($cm->id, 'forum', $forum->id, null);
  257. // Delete digest and subscription preferences.
  258. $DB->delete_records('forum_digests', array('forum' => $forum->id));
  259. $DB->delete_records('forum_subscriptions', array('forum'=>$forum->id));
  260. $DB->delete_records('forum_discussion_subs', array('forum' => $forum->id));
  261. if ($discussions = $DB->get_records('forum_discussions', array('forum'=>$forum->id))) {
  262. foreach ($discussions as $discussion) {
  263. if (!forum_delete_discussion($discussion, true, $course, $cm, $forum)) {
  264. $result = false;
  265. }
  266. }
  267. }
  268. forum_tp_delete_read_records(-1, -1, -1, $forum->id);
  269. forum_grade_item_delete($forum);
  270. // We must delete the module record after we delete the grade item.
  271. if (!$DB->delete_records('forum', array('id'=>$forum->id))) {
  272. $result = false;
  273. }
  274. return $result;
  275. }
  276. /**
  277. * Indicates API features that the forum supports.
  278. *
  279. * @uses FEATURE_GROUPS
  280. * @uses FEATURE_GROUPINGS
  281. * @uses FEATURE_MOD_INTRO
  282. * @uses FEATURE_COMPLETION_TRACKS_VIEWS
  283. * @uses FEATURE_COMPLETION_HAS_RULES
  284. * @uses FEATURE_GRADE_HAS_GRADE
  285. * @uses FEATURE_GRADE_OUTCOMES
  286. * @param string $feature
  287. * @return mixed True if yes (some features may use other values)
  288. */
  289. function forum_supports($feature) {
  290. switch($feature) {
  291. case FEATURE_GROUPS: return true;
  292. case FEATURE_GROUPINGS: return true;
  293. case FEATURE_MOD_INTRO: return true;
  294. case FEATURE_COMPLETION_TRACKS_VIEWS: return true;
  295. case FEATURE_COMPLETION_HAS_RULES: return true;
  296. case FEATURE_GRADE_HAS_GRADE: return true;
  297. case FEATURE_GRADE_OUTCOMES: return true;
  298. case FEATURE_RATE: return true;
  299. case FEATURE_BACKUP_MOODLE2: return true;
  300. case FEATURE_SHOW_DESCRIPTION: return true;
  301. case FEATURE_PLAGIARISM: return true;
  302. case FEATURE_ADVANCED_GRADING: return true;
  303. default: return null;
  304. }
  305. }
  306. /**
  307. * Create a message-id string to use in the custom headers of forum notification emails
  308. *
  309. * message-id is used by email clients to identify emails and to nest conversations
  310. *
  311. * @param int $postid The ID of the forum post we are notifying the user about
  312. * @param int $usertoid The ID of the user being notified
  313. * @return string A unique message-id
  314. */
  315. function forum_get_email_message_id($postid, $usertoid) {
  316. return generate_email_messageid(hash('sha256', $postid . 'to' . $usertoid));
  317. }
  318. /**
  319. *
  320. * @param object $course
  321. * @param object $user
  322. * @param object $mod TODO this is not used in this function, refactor
  323. * @param object $forum
  324. * @return object A standard object with 2 variables: info (number of posts for this user) and time (last modified)
  325. */
  326. function forum_user_outline($course, $user, $mod, $forum) {
  327. global $CFG;
  328. require_once("$CFG->libdir/gradelib.php");
  329. $gradeinfo = '';
  330. $gradetime = 0;
  331. $grades = grade_get_grades($course->id, 'mod', 'forum', $forum->id, $user->id);
  332. if (!empty($grades->items[0]->grades)) {
  333. // Item 0 is the rating.
  334. $grade = reset($grades->items[0]->grades);
  335. $gradetime = max($gradetime, grade_get_date_for_user_grade($grade, $user));
  336. if (!$grade->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) {
  337. $gradeinfo .= get_string('gradeforrating', 'forum', $grade) . html_writer::empty_tag('br');
  338. } else {
  339. $gradeinfo .= get_string('gradeforratinghidden', 'forum') . html_writer::empty_tag('br');
  340. }
  341. }
  342. // Item 1 is the whole-forum grade.
  343. if (!empty($grades->items[1]->grades)) {
  344. $grade = reset($grades->items[1]->grades);
  345. $gradetime = max($gradetime, grade_get_date_for_user_grade($grade, $user));
  346. if (!$grade->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) {
  347. $gradeinfo .= get_string('gradeforwholeforum', 'forum', $grade) . html_writer::empty_tag('br');
  348. } else {
  349. $gradeinfo .= get_string('gradeforwholeforumhidden', 'forum') . html_writer::empty_tag('br');
  350. }
  351. }
  352. $count = forum_count_user_posts($forum->id, $user->id);
  353. if ($count && $count->postcount > 0) {
  354. $info = get_string("numposts", "forum", $count->postcount);
  355. $time = $count->lastpost;
  356. if ($gradeinfo) {
  357. $info .= ', ' . $gradeinfo;
  358. $time = max($time, $gradetime);
  359. }
  360. return (object) [
  361. 'info' => $info,
  362. 'time' => $time,
  363. ];
  364. } else if ($gradeinfo) {
  365. return (object) [
  366. 'info' => $gradeinfo,
  367. 'time' => $gradetime,
  368. ];
  369. }
  370. return null;
  371. }
  372. /**
  373. * @global object
  374. * @global object
  375. * @param object $coure
  376. * @param object $user
  377. * @param object $mod
  378. * @param object $forum
  379. */
  380. function forum_user_complete($course, $user, $mod, $forum) {
  381. global $CFG, $USER;
  382. require_once("$CFG->libdir/gradelib.php");
  383. $getgradeinfo = function($grades, string $type) use ($course): string {
  384. global $OUTPUT;
  385. if (empty($grades)) {
  386. return '';
  387. }
  388. $result = '';
  389. $grade = reset($grades);
  390. if (!$grade->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) {
  391. $result .= $OUTPUT->container(get_string("gradefor{$type}", "forum", $grade));
  392. if ($grade->str_feedback) {
  393. $result .= $OUTPUT->container(get_string('feedback').': '.$grade->str_feedback);
  394. }
  395. } else {
  396. $result .= $OUTPUT->container(get_string("gradefor{$type}hidden", "forum"));
  397. }
  398. return $result;
  399. };
  400. $grades = grade_get_grades($course->id, 'mod', 'forum', $forum->id, $user->id);
  401. // Item 0 is the rating.
  402. if (!empty($grades->items[0]->grades)) {
  403. echo $getgradeinfo($grades->items[0]->grades, 'rating');
  404. }
  405. // Item 1 is the whole-forum grade.
  406. if (!empty($grades->items[1]->grades)) {
  407. echo $getgradeinfo($grades->items[1]->grades, 'wholeforum');
  408. }
  409. if ($posts = forum_get_user_posts($forum->id, $user->id)) {
  410. if (!$cm = get_coursemodule_from_instance('forum', $forum->id, $course->id)) {
  411. print_error('invalidcoursemodule');
  412. }
  413. $context = context_module::instance($cm->id);
  414. $discussions = forum_get_user_involved_discussions($forum->id, $user->id);
  415. $posts = array_filter($posts, function($post) use ($discussions) {
  416. return isset($discussions[$post->discussion]);
  417. });
  418. $entityfactory = mod_forum\local\container::get_entity_factory();
  419. $rendererfactory = mod_forum\local\container::get_renderer_factory();
  420. $postrenderer = $rendererfactory->get_posts_renderer();
  421. echo $postrenderer->render(
  422. $USER,
  423. [$forum->id => $entityfactory->get_forum_from_stdclass($forum, $context, $cm, $course)],
  424. array_map(function($discussion) use ($entityfactory) {
  425. return $entityfactory->get_discussion_from_stdclass($discussion);
  426. }, $discussions),
  427. array_map(function($post) use ($entityfactory) {
  428. return $entityfactory->get_post_from_stdclass($post);
  429. }, $posts)
  430. );
  431. } else {
  432. echo "<p>".get_string("noposts", "forum")."</p>";
  433. }
  434. }
  435. /**
  436. * @deprecated since Moodle 3.3, when the block_course_overview block was removed.
  437. */
  438. function forum_filter_user_groups_discussions() {
  439. throw new coding_exception('forum_filter_user_groups_discussions() can not be used any more and is obsolete.');
  440. }
  441. /**
  442. * Returns whether the discussion group is visible by the current user or not.
  443. *
  444. * @since Moodle 2.8, 2.7.1, 2.6.4
  445. * @param cm_info $cm The discussion course module
  446. * @param int $discussiongroupid The discussion groupid
  447. * @return bool
  448. */
  449. function forum_is_user_group_discussion(cm_info $cm, $discussiongroupid) {
  450. if ($discussiongroupid == -1 || $cm->effectivegroupmode != SEPARATEGROUPS) {
  451. return true;
  452. }
  453. if (isguestuser()) {
  454. return false;
  455. }
  456. if (has_capability('moodle/site:accessallgroups', context_module::instance($cm->id)) ||
  457. in_array($discussiongroupid, $cm->get_modinfo()->get_groups($cm->groupingid))) {
  458. return true;
  459. }
  460. return false;
  461. }
  462. /**
  463. * @deprecated since Moodle 3.3, when the block_course_overview block was removed.
  464. */
  465. function forum_print_overview() {
  466. throw new coding_exception('forum_print_overview() can not be used any more and is obsolete.');
  467. }
  468. /**
  469. * Given a course and a date, prints a summary of all the new
  470. * messages posted in the course since that date
  471. *
  472. * @global object
  473. * @global object
  474. * @global object
  475. * @uses CONTEXT_MODULE
  476. * @uses VISIBLEGROUPS
  477. * @param object $course
  478. * @param bool $viewfullnames capability
  479. * @param int $timestart
  480. * @return bool success
  481. */
  482. function forum_print_recent_activity($course, $viewfullnames, $timestart) {
  483. global $USER, $DB, $OUTPUT;
  484. // do not use log table if possible, it may be huge and is expensive to join with other tables
  485. $userfieldsapi = \core_user\fields::for_userpic();
  486. $allnamefields = $userfieldsapi->get_sql('u', false, '', 'duserid', false)->selects;
  487. if (!$posts = $DB->get_records_sql("SELECT p.*,
  488. f.course, f.type AS forumtype, f.name AS forumname, f.intro, f.introformat, f.duedate,
  489. f.cutoffdate, f.assessed AS forumassessed, f.assesstimestart, f.assesstimefinish,
  490. f.scale, f.grade_forum, f.maxbytes, f.maxattachments, f.forcesubscribe,
  491. f.trackingtype, f.rsstype, f.rssarticles, f.timemodified, f.warnafter, f.blockafter,
  492. f.blockperiod, f.completiondiscussions, f.completionreplies, f.completionposts,
  493. f.displaywordcount, f.lockdiscussionafter, f.grade_forum_notify,
  494. d.name AS discussionname, d.firstpost, d.userid AS discussionstarter,
  495. d.assessed AS discussionassessed, d.timemodified, d.usermodified, d.forum, d.groupid,
  496. d.timestart, d.timeend, d.pinned, d.timelocked,
  497. $allnamefields
  498. FROM {forum_posts} p
  499. JOIN {forum_discussions} d ON d.id = p.discussion
  500. JOIN {forum} f ON f.id = d.forum
  501. JOIN {user} u ON u.id = p.userid
  502. WHERE p.created > ? AND f.course = ? AND p.deleted <> 1
  503. ORDER BY p.id ASC", array($timestart, $course->id))) { // order by initial posting date
  504. return false;
  505. }
  506. $modinfo = get_fast_modinfo($course);
  507. $strftimerecent = get_string('strftimerecent');
  508. $managerfactory = mod_forum\local\container::get_manager_factory();
  509. $entityfactory = mod_forum\local\container::get_entity_factory();
  510. $discussions = [];
  511. $capmanagers = [];
  512. $printposts = [];
  513. foreach ($posts as $post) {
  514. if (!isset($modinfo->instances['forum'][$post->forum])) {
  515. // not visible
  516. continue;
  517. }
  518. $cm = $modinfo->instances['forum'][$post->forum];
  519. if (!$cm->uservisible) {
  520. continue;
  521. }
  522. // Get the discussion. Cache if not yet available.
  523. if (!isset($discussions[$post->discussion])) {
  524. // Build the discussion record object from the post data.
  525. $discussionrecord = (object)[
  526. 'id' => $post->discussion,
  527. 'course' => $post->course,
  528. 'forum' => $post->forum,
  529. 'name' => $post->discussionname,
  530. 'firstpost' => $post->firstpost,
  531. 'userid' => $post->discussionstarter,
  532. 'groupid' => $post->groupid,
  533. 'assessed' => $post->discussionassessed,
  534. 'timemodified' => $post->timemodified,
  535. 'usermodified' => $post->usermodified,
  536. 'timestart' => $post->timestart,
  537. 'timeend' => $post->timeend,
  538. 'pinned' => $post->pinned,
  539. 'timelocked' => $post->timelocked
  540. ];
  541. // Build the discussion entity from the factory and cache it.
  542. $discussions[$post->discussion] = $entityfactory->get_discussion_from_stdclass($discussionrecord);
  543. }
  544. $discussionentity = $discussions[$post->discussion];
  545. // Get the capability manager. Cache if not yet available.
  546. if (!isset($capmanagers[$post->forum])) {
  547. $context = context_module::instance($cm->id);
  548. $coursemodule = $cm->get_course_module_record();
  549. // Build the forum record object from the post data.
  550. $forumrecord = (object)[
  551. 'id' => $post->forum,
  552. 'course' => $post->course,
  553. 'type' => $post->forumtype,
  554. 'name' => $post->forumname,
  555. 'intro' => $post->intro,
  556. 'introformat' => $post->introformat,
  557. 'duedate' => $post->duedate,
  558. 'cutoffdate' => $post->cutoffdate,
  559. 'assessed' => $post->forumassessed,
  560. 'assesstimestart' => $post->assesstimestart,
  561. 'assesstimefinish' => $post->assesstimefinish,
  562. 'scale' => $post->scale,
  563. 'grade_forum' => $post->grade_forum,
  564. 'maxbytes' => $post->maxbytes,
  565. 'maxattachments' => $post->maxattachments,
  566. 'forcesubscribe' => $post->forcesubscribe,
  567. 'trackingtype' => $post->trackingtype,
  568. 'rsstype' => $post->rsstype,
  569. 'rssarticles' => $post->rssarticles,
  570. 'timemodified' => $post->timemodified,
  571. 'warnafter' => $post->warnafter,
  572. 'blockafter' => $post->blockafter,
  573. 'blockperiod' => $post->blockperiod,
  574. 'completiondiscussions' => $post->completiondiscussions,
  575. 'completionreplies' => $post->completionreplies,
  576. 'completionposts' => $post->completionposts,
  577. 'displaywordcount' => $post->displaywordcount,
  578. 'lockdiscussionafter' => $post->lockdiscussionafter,
  579. 'grade_forum_notify' => $post->grade_forum_notify
  580. ];
  581. // Build the forum entity from the factory.
  582. $forumentity = $entityfactory->get_forum_from_stdclass($forumrecord, $context, $coursemodule, $course);
  583. // Get the capability manager of this forum and cache it.
  584. $capmanagers[$post->forum] = $managerfactory->get_capability_manager($forumentity);
  585. }
  586. $capabilitymanager = $capmanagers[$post->forum];
  587. // Get the post entity.
  588. $postentity = $entityfactory->get_post_from_stdclass($post);
  589. // Check if the user can view the post.
  590. if ($capabilitymanager->can_view_post($USER, $discussionentity, $postentity)) {
  591. $printposts[] = $post;
  592. }
  593. }
  594. unset($posts);
  595. if (!$printposts) {
  596. return false;
  597. }
  598. echo $OUTPUT->heading(get_string('newforumposts', 'forum') . ':', 6);
  599. $list = html_writer::start_tag('ul', ['class' => 'unlist']);
  600. foreach ($printposts as $post) {
  601. $subjectclass = empty($post->parent) ? ' bold' : '';
  602. $authorhidden = forum_is_author_hidden($post, (object) ['type' => $post->forumtype]);
  603. $list .= html_writer::start_tag('li');
  604. $list .= html_writer::start_div('head');
  605. $list .= html_writer::div(userdate_htmltime($post->modified, $strftimerecent), 'date');
  606. if (!$authorhidden) {
  607. $list .= html_writer::div(fullname($post, $viewfullnames), 'name');
  608. }
  609. $list .= html_writer::end_div(); // Head.
  610. $list .= html_writer::start_div('info' . $subjectclass);
  611. $discussionurl = new moodle_url('/mod/forum/discuss.php', ['d' => $post->discussion]);
  612. if (!empty($post->parent)) {
  613. $discussionurl->param('parent', $post->parent);
  614. $discussionurl->set_anchor('p'. $post->id);
  615. }
  616. $post->subject = break_up_long_words(format_string($post->subject, true));
  617. $list .= html_writer::link($discussionurl, $post->subject, ['rel' => 'bookmark']);
  618. $list .= html_writer::end_div(); // Info.
  619. $list .= html_writer::end_tag('li');
  620. }
  621. $list .= html_writer::end_tag('ul');
  622. echo $list;
  623. return true;
  624. }
  625. /**
  626. * Update activity grades.
  627. *
  628. * @param object $forum
  629. * @param int $userid specific user only, 0 means all
  630. */
  631. function forum_update_grades($forum, $userid = 0): void {
  632. global $CFG, $DB;
  633. require_once($CFG->libdir.'/gradelib.php');
  634. $ratings = null;
  635. if ($forum->assessed) {
  636. require_once($CFG->dirroot.'/rating/lib.php');
  637. $cm = get_coursemodule_from_instance('forum', $forum->id);
  638. $rm = new rating_manager();
  639. $ratings = $rm->get_user_grades((object) [
  640. 'component' => 'mod_forum',
  641. 'ratingarea' => 'post',
  642. 'contextid' => \context_module::instance($cm->id)->id,
  643. 'modulename' => 'forum',
  644. 'moduleid ' => $forum->id,
  645. 'userid' => $userid,
  646. 'aggregationmethod' => $forum->assessed,
  647. 'scaleid' => $forum->scale,
  648. 'itemtable' => 'forum_posts',
  649. 'itemtableusercolumn' => 'userid',
  650. ]);
  651. }
  652. $forumgrades = null;
  653. if ($forum->grade_forum) {
  654. $sql = <<<EOF
  655. SELECT
  656. g.userid,
  657. 0 as datesubmitted,
  658. g.grade as rawgrade,
  659. g.timemodified as dategraded
  660. FROM {forum} f
  661. JOIN {forum_grades} g ON g.forum = f.id
  662. WHERE f.id = :forumid
  663. EOF;
  664. $params = [
  665. 'forumid' => $forum->id,
  666. ];
  667. if ($userid) {
  668. $sql .= " AND g.userid = :userid";
  669. $params['userid'] = $userid;
  670. }
  671. $forumgrades = [];
  672. if ($grades = $DB->get_recordset_sql($sql, $params)) {
  673. foreach ($grades as $userid => $grade) {
  674. if ($grade->rawgrade != -1) {
  675. $forumgrades[$userid] = $grade;
  676. }
  677. }
  678. $grades->close();
  679. }
  680. }
  681. forum_grade_item_update($forum, $ratings, $forumgrades);
  682. }
  683. /**
  684. * Create/update grade items for given forum.
  685. *
  686. * @param stdClass $forum Forum object with extra cmidnumber
  687. * @param mixed $grades Optional array/object of grade(s); 'reset' means reset grades in gradebook
  688. */
  689. function forum_grade_item_update($forum, $ratings = null, $forumgrades = null): void {
  690. global $CFG;
  691. require_once("{$CFG->libdir}/gradelib.php");
  692. // Update the rating.
  693. $item = [
  694. 'itemname' => get_string('gradeitemnameforrating', 'forum', $forum),
  695. 'idnumber' => $forum->cmidnumber,
  696. ];
  697. if (!$forum->assessed || $forum->scale == 0) {
  698. $item['gradetype'] = GRADE_TYPE_NONE;
  699. } else if ($forum->scale > 0) {
  700. $item['gradetype'] = GRADE_TYPE_VALUE;
  701. $item['grademax'] = $forum->scale;
  702. $item['grademin'] = 0;
  703. } else if ($forum->scale < 0) {
  704. $item['gradetype'] = GRADE_TYPE_SCALE;
  705. $item['scaleid'] = -$forum->scale;
  706. }
  707. if ($ratings === 'reset') {
  708. $item['reset'] = true;
  709. $ratings = null;
  710. }
  711. // Itemnumber 0 is the rating.
  712. grade_update('mod/forum', $forum->course, 'mod', 'forum', $forum->id, 0, $ratings, $item);
  713. // Whole forum grade.
  714. $item = [
  715. 'itemname' => get_string('gradeitemnameforwholeforum', 'forum', $forum),
  716. // Note: We do not need to store the idnumber here.
  717. ];
  718. if (!$forum->grade_forum) {
  719. $item['gradetype'] = GRADE_TYPE_NONE;
  720. } else if ($forum->grade_forum > 0) {
  721. $item['gradetype'] = GRADE_TYPE_VALUE;
  722. $item['grademax'] = $forum->grade_forum;
  723. $item['grademin'] = 0;
  724. } else if ($forum->grade_forum < 0) {
  725. $item['gradetype'] = GRADE_TYPE_SCALE;
  726. $item['scaleid'] = $forum->grade_forum * -1;
  727. }
  728. if ($forumgrades === 'reset') {
  729. $item['reset'] = true;
  730. $forumgrades = null;
  731. }
  732. // Itemnumber 1 is the whole forum grade.
  733. grade_update('mod/forum', $forum->course, 'mod', 'forum', $forum->id, 1, $forumgrades, $item);
  734. }
  735. /**
  736. * Delete grade item for given forum.
  737. *
  738. * @param stdClass $forum Forum object
  739. */
  740. function forum_grade_item_delete($forum) {
  741. global $CFG;
  742. require_once($CFG->libdir.'/gradelib.php');
  743. grade_update('mod/forum', $forum->course, 'mod', 'forum', $forum->id, 0, null, ['deleted' => 1]);
  744. grade_update('mod/forum', $forum->course, 'mod', 'forum', $forum->id, 1, null, ['deleted' => 1]);
  745. }
  746. /**
  747. * Checks if scale is being used by any instance of forum.
  748. *
  749. * This is used to find out if scale used anywhere.
  750. *
  751. * @param $scaleid int
  752. * @return boolean True if the scale is used by any forum
  753. */
  754. function forum_scale_used_anywhere(int $scaleid): bool {
  755. global $DB;
  756. if (empty($scaleid)) {
  757. return false;
  758. }
  759. return $DB->record_exists_select('forum', "scale = ? and assessed > 0", [$scaleid * -1]);
  760. }
  761. // SQL FUNCTIONS ///////////////////////////////////////////////////////////
  762. /**
  763. * Gets a post with all info ready for forum_print_post
  764. * Most of these joins are just to get the forum id
  765. *
  766. * @global object
  767. * @global object
  768. * @param int $postid
  769. * @return mixed array of posts or false
  770. */
  771. function forum_get_post_full($postid) {
  772. global $CFG, $DB;
  773. $userfieldsapi = \core_user\fields::for_name();
  774. $allnames = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
  775. return $DB->get_record_sql("SELECT p.*, d.forum, $allnames, u.email, u.picture, u.imagealt
  776. FROM {forum_posts} p
  777. JOIN {forum_discussions} d ON p.discussion = d.id
  778. LEFT JOIN {user} u ON p.userid = u.id
  779. WHERE p.id = ?", array($postid));
  780. }
  781. /**
  782. * Gets all posts in discussion including top parent.
  783. *
  784. * @param int $discussionid The Discussion to fetch.
  785. * @param string $sort The sorting to apply.
  786. * @param bool $tracking Whether the user tracks this forum.
  787. * @return array The posts in the discussion.
  788. */
  789. function forum_get_all_discussion_posts($discussionid, $sort, $tracking = false) {
  790. global $CFG, $DB, $USER;
  791. $tr_sel = "";
  792. $tr_join = "";
  793. $params = array();
  794. if ($tracking) {
  795. $tr_sel = ", fr.id AS postread";
  796. $tr_join = "LEFT JOIN {forum_read} fr ON (fr.postid = p.id AND fr.userid = ?)";
  797. $params[] = $USER->id;
  798. }
  799. $userfieldsapi = \core_user\fields::for_name();
  800. $allnames = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
  801. $params[] = $discussionid;
  802. if (!$posts = $DB->get_records_sql("SELECT p.*, $allnames, u.email, u.picture, u.imagealt $tr_sel
  803. FROM {forum_posts} p
  804. LEFT JOIN {user} u ON p.userid = u.id
  805. $tr_join
  806. WHERE p.discussion = ?
  807. ORDER BY $sort", $params)) {
  808. return array();
  809. }
  810. foreach ($posts as $pid=>$p) {
  811. if ($tracking) {
  812. if (forum_tp_is_post_old($p)) {
  813. $posts[$pid]->postread = true;
  814. }
  815. }
  816. if (!$p->parent) {
  817. continue;
  818. }
  819. if (!isset($posts[$p->parent])) {
  820. continue; // parent does not exist??
  821. }
  822. if (!isset($posts[$p->parent]->children)) {
  823. $posts[$p->parent]->children = array();
  824. }
  825. $posts[$p->parent]->children[$pid] =& $posts[$pid];
  826. }
  827. // Start with the last child of the first post.
  828. $post = &$posts[reset($posts)->id];
  829. $lastpost = false;
  830. while (!$lastpost) {
  831. if (!isset($post->children)) {
  832. $post->lastpost = true;
  833. $lastpost = true;
  834. } else {
  835. // Go to the last child of this post.
  836. $post = &$posts[end($post->children)->id];
  837. }
  838. }
  839. return $posts;
  840. }
  841. /**
  842. * An array of forum objects that the user is allowed to read/search through.
  843. *
  844. * @global object
  845. * @global object
  846. * @global object
  847. * @param int $userid
  848. * @param int $courseid if 0, we look for forums throughout the whole site.
  849. * @return array of forum objects, or false if no matches
  850. * Forum objects have the following attributes:
  851. * id, type, course, cmid, cmvisible, cmgroupmode, accessallgroups,
  852. * viewhiddentimedposts
  853. */
  854. function forum_get_readable_forums($userid, $courseid=0) {
  855. global $CFG, $DB, $USER;
  856. require_once($CFG->dirroot.'/course/lib.php');
  857. if (!$forummod = $DB->get_record('modules', array('name' => 'forum'))) {
  858. print_error('notinstalled', 'forum');
  859. }
  860. if ($courseid) {
  861. $courses = $DB->get_records('course', array('id' => $courseid));
  862. } else {
  863. // If no course is specified, then the user can see SITE + his courses.
  864. $courses1 = $DB->get_records('course', array('id' => SITEID));
  865. $courses2 = enrol_get_users_courses($userid, true, array('modinfo'));
  866. $courses = array_merge($courses1, $courses2);
  867. }
  868. if (!$courses) {
  869. return array();
  870. }
  871. $readableforums = array();
  872. foreach ($courses as $course) {
  873. $modinfo = get_fast_modinfo($course);
  874. if (empty($modinfo->instances['forum'])) {
  875. // hmm, no forums?
  876. continue;
  877. }
  878. $courseforums = $DB->get_records('forum', array('course' => $course->id));
  879. foreach ($modinfo->instances['forum'] as $forumid => $cm) {
  880. if (!$cm->uservisible or !isset($courseforums[$forumid])) {
  881. continue;
  882. }
  883. $context = context_module::instance($cm->id);
  884. $forum = $courseforums[$forumid];
  885. $forum->context = $context;
  886. $forum->cm = $cm;
  887. if (!has_capability('mod/forum:viewdiscussion', $context)) {
  888. continue;
  889. }
  890. /// group access
  891. if (groups_get_activity_groupmode($cm, $course) == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $context)) {
  892. $forum->onlygroups = $modinfo->get_groups($cm->groupingid);
  893. $forum->onlygroups[] = -1;
  894. }
  895. /// hidden timed discussions
  896. $forum->viewhiddentimedposts = true;
  897. if (!empty($CFG->forum_enabletimedposts)) {
  898. if (!has_capability('mod/forum:viewhiddentimedposts', $context)) {
  899. $forum->viewhiddentimedposts = false;
  900. }
  901. }
  902. /// qanda access
  903. if ($forum->type == 'qanda'
  904. && !has_capability('mod/forum:viewqandawithoutposting', $context)) {
  905. // We need to check whether the user has posted in the qanda forum.
  906. $forum->onlydiscussions = array(); // Holds discussion ids for the discussions
  907. // the user is allowed to see in this forum.
  908. if ($discussionspostedin = forum_discussions_user_has_posted_in($forum->id, $USER->id)) {
  909. foreach ($discussionspostedin as $d) {
  910. $forum->onlydiscussions[] = $d->id;
  911. }
  912. }
  913. }
  914. $readableforums[$forum->id] = $forum;
  915. }
  916. unset($modinfo);
  917. } // End foreach $courses
  918. return $readableforums;
  919. }
  920. /**
  921. * Returns a list of posts found using an array of search terms.
  922. *
  923. * @global object
  924. * @global object
  925. * @global object
  926. * @param array $searchterms array of search terms, e.g. word +word -word
  927. * @param int $courseid if 0, we search through the whole site
  928. * @param int $limitfrom
  929. * @param int $limitnum
  930. * @param int &$totalcount
  931. * @param string $extrasql
  932. * @return array|bool Array of posts found or false
  933. */
  934. function forum_search_posts($searchterms, $courseid, $limitfrom, $limitnum,
  935. &$totalcount, $extrasql='') {
  936. global $CFG, $DB, $USER;
  937. require_once($CFG->libdir.'/searchlib.php');
  938. $forums = forum_get_readable_forums($USER->id, $courseid);
  939. if (count($forums) == 0) {
  940. $totalcount = 0;
  941. return false;
  942. }
  943. $now = floor(time() / 60) * 60; // DB Cache Friendly.
  944. $fullaccess = array();
  945. $where = array();
  946. $params = array();
  947. foreach ($forums as $forumid => $forum) {
  948. $select = array();
  949. if (!$forum->viewhiddentimedposts) {
  950. $select[] = "(d.userid = :userid{$forumid} OR (d.timestart < :timestart{$forumid} AND (d.timeend = 0 OR d.timeend > :timeend{$forumid})))";
  951. $params = array_merge($params, array('userid'.$forumid=>$USER->id, 'timestart'.$forumid=>$now, 'timeend'.$forumid=>$now));
  952. }
  953. $cm = $forum->cm;
  954. $context = $forum->context;
  955. if ($forum->type == 'qanda'
  956. && !has_capability('mod/forum:viewqandawithoutposting', $context)) {
  957. if (!empty($forum->onlydiscussions)) {
  958. list($discussionid_sql, $discussionid_params) = $DB->get_in_or_equal($forum->onlydiscussions, SQL_PARAMS_NAMED, 'qanda'.$forumid.'_');
  959. $params = array_merge($params, $discussionid_params);
  960. $select[] = "(d.id $discussionid_sql OR p.parent = 0)";
  961. } else {
  962. $select[] = "p.parent = 0";
  963. }
  964. }
  965. if (!empty($forum->onlygroups)) {
  966. list($groupid_sql, $groupid_params) = $DB->get_in_or_equal($forum->onlygroups, SQL_PARAMS_NAMED, 'grps'.$forumid.'_');
  967. $params = array_merge($params, $groupid_params);
  968. $select[] = "d.groupid $groupid_sql";
  969. }
  970. if ($select) {
  971. $selects = implode(" AND ", $select);
  972. $where[] = "(d.forum = :forum{$forumid} AND $selects)";
  973. $params['forum'.$forumid] = $forumid;
  974. } else {
  975. $fullaccess[] = $forumid;
  976. }
  977. }
  978. if ($fullaccess) {
  979. list($fullid_sql, $fullid_params) = $DB->get_in_or_equal($fullaccess, SQL_PARAMS_NAMED, 'fula');
  980. $params = array_merge($params, $fullid_params);
  981. $where[] = "(d.forum $fullid_sql)";
  982. }
  983. $favjoin = "";
  984. if (in_array('starredonly:on', $searchterms)) {
  985. $usercontext = context_user::instance($USER->id);
  986. $ufservice = \core_favourites\service_factory::get_service_for_user_context($usercontext);
  987. list($favjoin, $favparams) = $ufservice->get_join_sql_by_type('mod_forum', 'discussions',
  988. "favourited", "d.id");
  989. $searchterms = array_values(array_diff($searchterms, array('starredonly:on')));
  990. $params = array_merge($params, $favparams);
  991. $extrasql .= " AND favourited.itemid IS NOT NULL AND favourited.itemid != 0";
  992. }
  993. $selectdiscussion = "(".implode(" OR ", $where).")";
  994. $messagesearch = '';
  995. $searchstring = '';
  996. // Need to concat these back together for parser to work.
  997. foreach($searchterms as $searchterm){
  998. if ($searchstring != '') {
  999. $searchstring .= ' ';
  1000. }
  1001. $searchstring .= $searchterm;
  1002. }
  1003. // We need to allow quoted strings for the search. The quotes *should* be stripped
  1004. // by the parser, but this should be examined carefully for security implications.
  1005. $searchstring = str_replace("\\\"","\"",$searchstring);
  1006. $parser = new search_parser();
  1007. $lexer = new search_lexer($parser);
  1008. if ($lexer->parse($searchstring)) {
  1009. $parsearray = $parser->get_parsed_array();
  1010. $tagjoins = '';
  1011. $tagfields = [];
  1012. $tagfieldcount = 0;
  1013. if ($parsearray) {
  1014. foreach ($parsearray as $token) {
  1015. if ($token->getType() == TOKEN_TAGS) {
  1016. for ($i = 0; $i <= substr_count($token->getValue(), ','); $i++) {
  1017. // Queries can only have a limited number of joins so set a limit sensible users won't exceed.
  1018. if ($tagfieldcount > 10) {
  1019. continue;
  1020. }
  1021. $tagjoins .= " LEFT JOIN {tag_instance} ti_$tagfieldcount
  1022. ON p.id = ti_$tagfieldcount.itemid
  1023. AND ti_$tagfieldcount.component = 'mod_forum'
  1024. AND ti_$tagfieldcount.itemtype = 'forum_posts'";
  1025. $tagjoins .= " LEFT JOIN {tag} t_$tagfieldcount ON t_$tagfieldcount.id = ti_$tagfieldcount.tagid";
  1026. $tagfields[] = "t_$tagfieldcount.rawname";
  1027. $tagfieldcount++;
  1028. }
  1029. }
  1030. }
  1031. list($messagesearch, $msparams) = search_generate_SQL($parsearray, 'p.message', 'p.subject',
  1032. 'p.userid', 'u.id', 'u.firstname',
  1033. 'u.lastname', 'p.modified', 'd.forum',
  1034. $tagfields);
  1035. $params = ($msparams ? array_merge($params, $msparams) : $params);
  1036. }
  1037. }
  1038. $fromsql = "{forum_posts} p
  1039. INNER JOIN {forum_discussions} d ON d.id = p.discussion
  1040. INNER JOIN {user} u ON u.id = p.userid $tagjoins $favjoin";
  1041. $selectsql = ($messagesearch ? $messagesearch . " AND " : "").
  1042. " p.discussion = d.id
  1043. AND p.userid = u.id
  1044. AND $selectdiscussion
  1045. $extrasql";
  1046. $countsql = "SELECT COUNT(*)
  1047. FROM $fromsql
  1048. WHERE $selectsql";
  1049. $userfieldsapi = \core_user\fields::for_name();
  1050. $allnames = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
  1051. $searchsql = "SELECT p.*,
  1052. d.forum,
  1053. $allnames,
  1054. u.email,
  1055. u.picture,
  1056. u.imagealt
  1057. FROM $fromsql
  1058. WHERE $selectsql
  1059. ORDER BY p.modified DESC";
  1060. $totalcount = $DB->count_records_sql($countsql, $params);
  1061. return $DB->get_records_sql($searchsql, $params, $limitfrom, $limitnum);
  1062. }
  1063. /**
  1064. * Get all the posts for a user in a forum suitable for forum_print_post
  1065. *
  1066. * @global object
  1067. * @global object
  1068. * @uses CONTEXT_MODULE
  1069. * @return array
  1070. */
  1071. function forum_get_user_posts($forumid, $userid) {
  1072. global $CFG, $DB;
  1073. $timedsql = "";
  1074. $params = array($forumid, $userid);
  1075. if (!empty($CFG->forum_enabletimedposts)) {
  1076. $cm = get_coursemodule_from_instance('forum', $forumid);
  1077. if (!has_capability('mod/forum:viewhiddentimedposts' , context_module::instance($cm->id))) {
  1078. $now = time();
  1079. $timedsql = "AND (d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?))";
  1080. $params[] = $now;
  1081. $params[] = $now;
  1082. }
  1083. }
  1084. $userfieldsapi = \core_user\fields::for_name();
  1085. $allnames = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
  1086. return $DB->get_records_sql("SELECT p.*, d.forum, $allnames, u.email, u.picture, u.imagealt
  1087. FROM {forum} f
  1088. JOIN {forum_discussions} d ON d.forum = f.id
  1089. JOIN {forum_posts} p ON p.discussion = d.id
  1090. JOIN {user} u ON u.id = p.userid
  1091. WHERE f.id = ?
  1092. AND p.userid = ?
  1093. $timedsql
  1094. ORDER BY p.modified ASC", $params);
  1095. }
  1096. /**
  1097. * Get all the discussions user participated in
  1098. *
  1099. * @global object
  1100. * @global object
  1101. * @uses CONTEXT_MODULE
  1102. * @param int $forumid
  1103. * @param int $userid
  1104. * @return array Array or false
  1105. */
  1106. function forum_get_user_involved_discussions($forumid, $userid) {
  1107. global $CFG, $DB;
  1108. $timedsql = "";
  1109. $params = array($forumid, $userid);
  1110. if (!empty($CFG->forum_enabletimedposts)) {
  1111. $cm = get_coursemodule_from_instance('forum', $forumid);
  1112. if (!has_capability('mod/forum:viewhiddentimedposts' , context_module::instance($cm->id))) {
  1113. $now = time();
  1114. $timedsql = "AND (d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?))";
  1115. $params[] = $now;
  1116. $params[] = $now;
  1117. }
  1118. }
  1119. return $DB->get_records_sql("SELECT DISTINCT d.*
  1120. FROM {forum} f
  1121. JOIN {forum_discussions} d ON d.forum = f.id
  1122. JOIN {forum_posts} p ON p.discussion = d.id
  1123. WHERE f.id = ?
  1124. AND p.userid = ?
  1125. $timedsql", $params);
  1126. }
  1127. /**
  1128. * Get all the posts for a user in a forum suitable for forum_print_post
  1129. *
  1130. * @global object
  1131. * @global object
  1132. * @param int $forumid
  1133. * @param int $userid
  1134. * @return array of counts or false
  1135. */
  1136. function forum_count_user_posts($forumid, $userid) {
  1137. global $CFG, $DB;
  1138. $timedsql = "";
  1139. $params = array($forumid, $userid);
  1140. if (!empty($CFG->forum_enabletimedposts)) {
  1141. $cm = get_coursemodule_from_instance('forum', $forumid);
  1142. if (!has_capability('mod/forum:viewhiddentimedposts' , context_module::instance($cm->id))) {
  1143. $now = time();
  1144. $timedsql = "AND (d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?))";
  1145. $params[] = $now;
  1146. $params[] = $now;
  1147. }
  1148. }
  1149. return $DB->get_record_sql("SELECT COUNT(p.id) AS postcount, MAX(p.modified) AS lastpost
  1150. FROM {forum} f
  1151. JOIN {forum_discussions} d ON d.forum = f.id
  1152. JOIN {forum_posts} p ON p.discussion = d.id
  1153. JOIN {user} u ON u.id = p.userid
  1154. WHERE f.id = ?
  1155. AND p.userid = ?
  1156. $timedsql", $params);
  1157. }
  1158. /**
  1159. * Given a log entry, return the forum post details for it.
  1160. *
  1161. * @global object
  1162. * @global object
  1163. * @param object $log
  1164. * @return array|null
  1165. */
  1166. function forum_get_post_from_log($log) {
  1167. global $CFG, $DB;
  1168. $userfieldsapi = \core_user\fields::for_name();
  1169. $allnames = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
  1170. if ($log->action == "add post") {
  1171. return $DB->get_record_sql("SELECT p.*, f.type AS forumtype, d.forum, d.groupid, $allnames, u.email, u.picture
  1172. FROM {forum_discussions} d,
  1173. {forum_posts} p,
  1174. {forum} f,
  1175. {user} u
  1176. WHERE p.id = ?
  1177. AND d.id = p.discussion
  1178. AND p.userid = u.id
  1179. AND u.deleted <> '1'
  1180. AND f.id = d.forum", array($log->info));
  1181. } else if ($log->action == "add discussion") {
  1182. return $DB->get_record_sql("SELECT p.*, f.type AS forumtype, d.forum, d.groupid, $allnames, u.email, u.picture
  1183. FROM {forum_discussions} d,
  1184. {forum_posts} p,
  1185. {forum} f,
  1186. {user} u
  1187. WHERE d.id = ?
  1188. AND d.firstpost = p.id
  1189. AND p.userid = u.id
  1190. AND u.deleted <> '1'
  1191. AND f.id = d.forum", array($log->info));
  1192. }
  1193. return NULL;
  1194. }
  1195. /**
  1196. * Given a discussion id, return the first post from the discussion
  1197. *
  1198. * @global object
  1199. * @global object
  1200. * @param int $dicsussionid
  1201. * @return array
  1202. */
  1203. function forum_get_firstpost_from_discussion($discussionid) {
  1204. global $CFG, $DB;
  1205. return $DB->get_record_sql("SELECT p.*
  1206. FROM {forum_discussions} d,
  1207. {forum_posts} p
  1208. WHERE d.id = ?
  1209. AND d.firstpost = p.id ", array($discussionid));
  1210. }
  1211. /**
  1212. * Returns an array of counts of replies to each discussion
  1213. *
  1214. * @param int $forumid
  1215. * @param string $forumsort
  1216. * @param int $limit
  1217. * @param int $page
  1218. * @param int $perpage
  1219. * @param boolean $canseeprivatereplies Whether the current user can see private replies.
  1220. * @return array
  1221. */
  1222. function forum_count_discussion_replies($forumid, $forumsort = "", $limit = -1, $page = -1, $perpage = 0,
  1223. $canseeprivatereplies = false) {
  1224. global $CFG, $DB, $USER;
  1225. if ($limit > 0) {
  1226. $limitfrom = 0;
  1227. $limitnum = $limit;
  1228. } else if ($page != -1) {
  1229. $limitfrom = $page*$perpage;
  1230. $limitnum = $perpage;
  1231. } else {
  1232. $limitfrom = 0;
  1233. $limitnum = 0;
  1234. }
  1235. if ($forumsort == "") {
  1236. $orderby = "";
  1237. $groupby = "";
  1238. } else {
  1239. $orderby = "ORDER BY $forumsort";
  1240. $groupby = ", ".strtolower($forumsort);
  1241. $groupby = str_replace('desc', '', $groupby);
  1242. $groupby = str_replace('asc', '', $groupby);
  1243. }
  1244. $params = ['forumid' => $forumid];
  1245. if (!$canseeprivatereplies) {
  1246. $privatewhere = ' AND (p.privatereplyto = :currentuser1 OR p.userid = :currentuser2 OR p.privatereplyto = 0)';
  1247. $params['currentuser1'] = $USER->id;
  1248. $params['currentuser2'] = $USER->id;
  1249. } else {
  1250. $privatewhere = '';
  1251. }
  1252. if (($limitfrom == 0 and $limitnum == 0) or $forumsort == "") {
  1253. $sql = "SELECT p.discussion, COUNT(p.id) AS replies, MAX(p.id) AS lastpostid
  1254. FROM {forum_posts} p
  1255. JOIN {forum_discussions} d ON p.discussion = d.id
  1256. WHERE p.parent > 0 AND d.forum = :forumid
  1257. $privatewhere
  1258. GROUP BY p.discussion";
  1259. return $DB->get_records_sql($sql, $params);
  1260. } else {
  1261. $sql = "SELECT p.discussion, (COUNT(p.id) - 1) AS replies, MAX(p.id) AS lastpostid
  1262. FROM {forum_posts} p
  1263. JOIN {forum_discussions} d ON p.discussion = d.id
  1264. WHERE d.forum = :forumid
  1265. $privatewhere
  1266. GROUP BY p.discussion $groupby $orderby";
  1267. return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
  1268. }
  1269. }
  1270. /**
  1271. * @global object
  1272. * @global object
  1273. * @global object
  1274. * @staticvar array $cache
  1275. * @param object $forum
  1276. * @param object $cm
  1277. * @param object $course
  1278. * @return mixed
  1279. */
  1280. function forum_count_discussions($forum, $cm, $course) {
  1281. global $CFG, $DB, $USER;
  1282. static $cache = array();
  1283. $now = floor(time() / 60) * 60; // DB Cache Friendly.
  1284. $params = array($course->id);
  1285. if (!isset($cache[$course->id])) {
  1286. if (!empty($CFG->forum_enabletimedposts)) {
  1287. $timedsql = "AND d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?)";
  1288. $params[] = $now;
  1289. $params[] = $now;
  1290. } else {
  1291. $timedsql = "";
  1292. }
  1293. $sql = "SELECT f.id, COUNT(d.id) as dcount
  1294. FROM {forum} f
  1295. JOIN {forum_discussions} d ON d.forum = f.id
  1296. WHERE f.course = ?
  1297. $timedsql
  1298. GROUP BY f.id";
  1299. if ($counts = $DB->get_records_sql($sql, $params)) {
  1300. foreach ($counts as $count) {
  1301. $counts[$count->id] = $count->dcount;
  1302. }
  1303. $cache[$course->id] = $counts;
  1304. } else {
  1305. $cache[$course->id] = array();
  1306. }
  1307. }
  1308. if (empty($cache[$course->id][$forum->id])) {
  1309. return 0;
  1310. }
  1311. $groupmode = groups_get_activity_groupmode($cm, $course);
  1312. if ($groupmode != SEPARATEGROUPS) {
  1313. return $cache[$course->id][$forum->id];
  1314. }
  1315. if (has_capability('moodle/site:accessallgroups', context_module::instance($cm->id))) {
  1316. return $cache[$course->id][$forum->id];
  1317. }
  1318. require_once($CFG->dirroot.'/course/lib.php');
  1319. $modinfo = get_fast_modinfo($course);
  1320. $mygroups = $modinfo->get_groups($cm->groupingid);
  1321. // add all groups posts
  1322. $mygroups[-1] = -1;
  1323. list($mygroups_sql, $params) = $DB->get_in_or_equal($mygroups);
  1324. $params[] = $forum->id;
  1325. if (!empty($CFG->forum_enabletimedposts)) {
  1326. $timedsql = "AND d.timestart < $now AND (d.timeend = 0 OR d.timeend > $now)";
  1327. $params[] = $now;
  1328. $params[] = $now;
  1329. } else {
  1330. $timedsql = "";
  1331. }
  1332. $sql = "SELECT COUNT(d.id)
  1333. FROM {forum_discussions} d
  1334. WHERE d.groupid $mygroups_sql AND d.forum = ?
  1335. $timedsql";
  1336. return $DB->get_field_sql($sql, $params);
  1337. }
  1338. /**
  1339. * Get all discussions in a forum
  1340. *
  1341. * @global object
  1342. * @global object
  1343. * @global object
  1344. * @uses CONTEXT_MODULE
  1345. * @uses VISIBLEGROUPS
  1346. * @param object $cm
  1347. * @param string $forumsort
  1348. * @param bool $fullpost
  1349. * @param int $unused
  1350. * @param int $limit
  1351. * @param bool $userlastmodified
  1352. * @param int $page
  1353. * @param int $perpage
  1354. * @param int $groupid if groups enabled, get discussions for this group overriding the current group.
  1355. * Use FORUM_POSTS_ALL_USER_GROUPS for all the user groups
  1356. * @param int $updatedsince retrieve only discussions updated since the given time
  1357. * @return array
  1358. */
  1359. function forum_get_discussions($cm, $forumsort="", $fullpost=true, $unused=-1, $limit=-1,
  1360. $userlastmodified=false, $page=-1, $perpage=0, $groupid = -1,
  1361. $updatedsince = 0) {
  1362. global $CFG, $DB, $USER;
  1363. $timelimit = '';
  1364. $now = floor(time() / 60) * 60;
  1365. $params = array($cm->instance);
  1366. $modcontext = context_module::instance($cm->id);
  1367. if (!has_capability('mod/forum:viewdiscussion', $modcontext)) { /// User must have perms to view discussions
  1368. return array();
  1369. }
  1370. if (!empty($CFG->forum_enabletimedposts)) { /// Users must fulfill timed posts
  1371. if (!has_capability('mod/forum:viewhiddentimedposts', $modcontext)) {
  1372. $timelimit = " AND ((d.timestart <= ? AND (d.timeend = 0 OR d.timeend > ?))";
  1373. $params[] = $now;
  1374. $params[] = $now;
  1375. if (isloggedin()) {
  1376. $timelimit .= " OR d.userid = ?";
  1377. $params[] = $USER->id;
  1378. }
  1379. $timelimit .= ")";
  1380. }
  1381. }
  1382. if ($limit > 0) {
  1383. $limitfrom = 0;
  1384. $limitnum = $limit;
  1385. } else if ($page != -1) {
  1386. $limitfrom = $page*$perpage;
  1387. $limitnum = $perpage;
  1388. } else {
  1389. $limitfrom = 0;
  1390. $limitnum = 0;
  1391. }
  1392. $groupmode = groups_get_activity_groupmode($cm);
  1393. if ($groupmode) {
  1394. if (empty($modcontext)) {
  1395. $modcontext = context_module::instance($cm->id);
  1396. }
  1397. // Special case, we received a groupid to override currentgroup.
  1398. if ($groupid > 0) {
  1399. $course = get_course($cm->course);
  1400. if (!groups_group_visible($groupid, $course, $cm)) {
  1401. // User doesn't belong to this group, return nothing.
  1402. return array();
  1403. }
  1404. $currentgroup = $groupid;
  1405. } else if ($groupid === -1) {
  1406. $currentgroup = groups_get_activity_group($cm);
  1407. } else {
  1408. // Get discussions for all groups current user can see.
  1409. $currentgroup = null;
  1410. }
  1411. if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $modcontext)) {
  1412. if ($currentgroup) {
  1413. $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
  1414. $params[] = $currentgroup;
  1415. } else {
  1416. $groupselect = "";
  1417. }
  1418. } else {
  1419. // Separate groups.
  1420. // Get discussions for all groups current user can see.
  1421. if ($currentgroup === null) {
  1422. $mygroups = array_keys(groups_get_all_groups($cm->course, $USER->id, $cm->groupingid, 'g.id'));
  1423. if (empty($mygroups)) {
  1424. $groupselect = "AND d.groupid = -1";
  1425. } else {
  1426. list($insqlgroups, $inparamsgroups) = $DB->get_in_or_equal($mygroups);
  1427. $groupselect = "AND (d.groupid = -1 OR d.groupid $insqlgroups)";
  1428. $params = array_merge($params, $inparamsgroups);
  1429. }
  1430. } else if ($currentgroup) {
  1431. $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
  1432. $params[] = $currentgroup;
  1433. } else {
  1434. $groupselect = "AND d.groupid = -1";
  1435. }
  1436. }
  1437. } else {
  1438. $groupselect = "";
  1439. }
  1440. if (empty($forumsort)) {
  1441. $forumsort = forum_get_default_sort_order();
  1442. }
  1443. if (empty($fullpost)) {
  1444. $postdata = "p.id, p.subject, p.modified, p.discussion, p.userid, p.created";
  1445. } else {
  1446. $postdata = "p.*";
  1447. }
  1448. $userfieldsapi = \core_user\fields::for_name();
  1449. if (empty($userlastmodified)) { // We don't need to know this
  1450. $umfields = "";
  1451. $umtable = "";
  1452. } else {
  1453. $umfields = $userfieldsapi->get_sql('um', false, 'um')->selects . ', um.email AS umemail, um.picture AS umpicture,
  1454. um.imagealt AS umimagealt';
  1455. $umtable = " LEFT JOIN {user} um ON (d.usermodified = um.id)";
  1456. }
  1457. $updatedsincesql = '';
  1458. if (!empty($updatedsince)) {
  1459. $updatedsincesql = 'AND d.timemodified > ?';
  1460. $params[] = $updatedsince;
  1461. }
  1462. $discussionfields = "d.id as discussionid, d.course, d.forum, d.name, d.firstpost, d.groupid, d.assessed," .
  1463. " d.timemodified, d.usermodified, d.timestart, d.timeend, d.pinned, d.timelocked";
  1464. $allnames = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
  1465. $sql = "SELECT $postdata, $discussionfields,
  1466. $allnames, u.email, u.picture, u.imagealt $umfields
  1467. FROM {forum_discussions} d
  1468. JOIN {forum_posts} p ON p.discussion = d.id
  1469. JOIN {user} u ON p.userid = u.id
  1470. $umtable
  1471. WHERE d.forum = ? AND p.parent = 0
  1472. $timelimit $groupselect $updatedsincesql
  1473. ORDER BY $forumsort, d.id DESC";
  1474. return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
  1475. }
  1476. /**
  1477. * Gets the neighbours (previous and next) of a discussion.
  1478. *
  1479. * The calculation is based on the timemodified when time modified or time created is identical
  1480. * It will revert to using the ID to sort consistently. This is better tha skipping a discussion.
  1481. *
  1482. * For blog-style forums, the calculation is based on the original creation time of the
  1483. * blog post.
  1484. *
  1485. * Please note that this does not check whether or not the discussion passed is accessible
  1486. * by the user, it simply uses it as a reference to find the neighbours. On the other hand,
  1487. * the returned neighbours are checked and are accessible to the current user.
  1488. *
  1489. * @param object $cm The CM record.
  1490. * @param object $discussion The discussion record.
  1491. * @param object $forum The forum instance record.
  1492. * @return array That always contains the keys 'prev' and 'next'. When there is a result
  1493. * they contain the record with minimal information such as 'id' and 'name'.
  1494. * When the neighbour is not found the value is false.
  1495. */
  1496. function forum_get_discussion_neighbours($cm, $discussion, $forum) {
  1497. global $CFG, $DB, $USER;
  1498. if ($cm->instance != $discussion->forum or $discussion->forum != $forum->id or $forum->id != $cm->instance) {
  1499. throw new coding_exception('Discussion is not part of the same forum.');
  1500. }
  1501. $neighbours = array('prev' => false, 'next' => false);
  1502. $now = floor(time() / 60) * 60;
  1503. $params = array();
  1504. $modcontext = context_module::instance($cm->id);
  1505. $groupmode = groups_get_activity_groupmode($cm);
  1506. $currentgroup = groups_get_activity_group($cm);
  1507. // Users must fulfill timed posts.
  1508. $timelimit = '';
  1509. if (!empty($CFG->forum_enabletimedposts)) {
  1510. if (!has_capability('mod/forum:viewhiddentimedposts', $modcontext)) {
  1511. $timelimit = ' AND ((d.timestart <= :tltimestart AND (d.timeend = 0 OR d.timeend > :tltimeend))';
  1512. $params['tltimestart'] = $now;
  1513. $params['tltimeend'] = $now;
  1514. if (isloggedin()) {
  1515. $timelimit .= ' OR d.userid = :tluserid';
  1516. $params['tluserid'] = $USER->id;
  1517. }
  1518. $timelimit .= ')';
  1519. }
  1520. }
  1521. // Limiting to posts accessible according to groups.
  1522. $groupselect = '';
  1523. if ($groupmode) {
  1524. if ($groupmode == VISIBLEGROUPS || has_capability('moodle/site:accessallgroups', $modcontext)) {
  1525. if ($currentgroup) {
  1526. $groupselect = 'AND (d.groupid = :groupid OR d.groupid = -1)';
  1527. $params['groupid'] = $currentgroup;
  1528. }
  1529. } else {
  1530. if ($currentgroup) {
  1531. $groupselect = 'AND (d.groupid = :groupid OR d.groupid = -1)';
  1532. $params['groupid'] = $currentgroup;
  1533. } else {
  1534. $groupselect = 'AND d.groupid = -1';
  1535. }
  1536. }
  1537. }
  1538. $params['forumid'] = $cm->instance;
  1539. $params['discid1'] = $discussion->id;
  1540. $params['discid2'] = $discussion->id;
  1541. $params['discid3'] = $discussion->id;
  1542. $params['discid4'] = $discussion->id;
  1543. $params['disctimecompare1'] = $discussion->timemodified;
  1544. $params['disctimecompare2'] = $discussion->timemodified;
  1545. $params['pinnedstate1'] = (int) $discussion->pinned;
  1546. $params['pinnedstate2'] = (int) $discussion->pinned;
  1547. $params['pinnedstate3'] = (int) $discussion->pinned;
  1548. $params['pinnedstate4'] = (int) $discussion->pinned;
  1549. $sql = "SELECT d.id, d.name, d.timemodified, d.groupid, d.timestart, d.timeend
  1550. FROM {forum_discussions} d
  1551. JOIN {forum_posts} p ON d.firstpost = p.id
  1552. WHERE d.forum = :forumid
  1553. AND d.id <> :discid1
  1554. $timelimit
  1555. $groupselect";
  1556. $comparefield = "d.timemodified";
  1557. $comparevalue = ":disctimecompare1";
  1558. $comparevalue2 = ":disctimecompare2";
  1559. if (!empty($CFG->forum_enabletimedposts)) {
  1560. // Here we need to take into account the release time (timestart)
  1561. // if one is set, of the neighbouring posts and compare it to the
  1562. // timestart or timemodified of *this* post depending on if the
  1563. // release date of this post is in the future or not.
  1564. // This stops discussions that appear later because of the
  1565. // timestart value from being buried under discussions that were
  1566. // made afterwards.
  1567. $comparefield = "CASE WHEN d.timemodified < d.timestart
  1568. THEN d.timestart ELSE d.timemodified END";
  1569. if ($discussion->timemodified < $discussion->timestart) {
  1570. // Normally we would just use the timemodified for sorting
  1571. // discussion posts. However, when timed discussions are enabled,
  1572. // then posts need to be sorted base on the later of timemodified
  1573. // or the release date of the post (timestart).
  1574. $params['disctimecompare1'] = $discussion->timestart;
  1575. $params['disctimecompare2'] = $discussion->timestart;
  1576. }
  1577. }
  1578. $orderbydesc = forum_get_default_sort_order(true, $comparefield, 'd', false);
  1579. $orderbyasc = forum_get_default_sort_order(false, $comparefield, 'd', false);
  1580. if ($forum->type === 'blog') {
  1581. $subselect = "SELECT pp.created
  1582. FROM {forum_discussions} dd
  1583. JOIN {forum_posts} pp ON dd.firstpost = pp.id ";
  1584. $subselectwhere1 = " WHERE dd.id = :discid3";
  1585. $subselectwhere2 = " WHERE dd.id = :discid4";
  1586. $comparefield = "p.created";
  1587. $sub1 = $subselect.$subselectwhere1;
  1588. $comparevalue = "($sub1)";
  1589. $sub2 = $subselect.$subselectwhere2;
  1590. $comparevalue2 = "($sub2)";
  1591. $orderbydesc = "d.pinned, p.created DESC";
  1592. $orderbyasc = "d.pinned, p.created ASC";
  1593. }
  1594. $prevsql = $sql . " AND ( (($comparefield < $comparevalue) AND :pinnedstate1 = d.pinned)
  1595. OR ($comparefield = $comparevalue2 AND (d.pinned = 0 OR d.pinned = :pinnedstate4) AND d.id < :discid2)
  1596. OR (d.pinned = 0 AND d.pinned <> :pinnedstate2))
  1597. ORDER BY CASE WHEN d.pinned = :pinnedstate3 THEN 1 ELSE 0 END DESC, $orderbydesc, d.id DESC";
  1598. $nextsql = $sql . " AND ( (($comparefield > $comparevalue) AND :pinnedstate1 = d.pinned)
  1599. OR ($comparefield = $comparevalue2 AND (d.pinned = 1 OR d.pinned = :pinnedstate4) AND d.id > :discid2)
  1600. OR (d.pinned = 1 AND d.pinned <> :pinnedstate2))
  1601. ORDER BY CASE WHEN d.pinned = :pinnedstate3 THEN 1 ELSE 0 END DESC, $orderbyasc, d.id ASC";
  1602. $neighbours['prev'] = $DB->get_record_sql($prevsql, $params, IGNORE_MULTIPLE);
  1603. $neighbours['next'] = $DB->get_record_sql($nextsql, $params, IGNORE_MULTIPLE);
  1604. return $neighbours;
  1605. }
  1606. /**
  1607. * Get the sql to use in the ORDER BY clause for forum discussions.
  1608. *
  1609. * This has the ordering take timed discussion windows into account.
  1610. *
  1611. * @param bool $desc True for DESC, False for ASC.
  1612. * @param string $compare The field in the SQL to compare to normally sort by.
  1613. * @param string $prefix The prefix being used for the discussion table.
  1614. * @param bool $pinned sort pinned posts to the top
  1615. * @return string
  1616. */
  1617. function forum_get_default_sort_order($desc = true, $compare = 'd.timemodified', $prefix = 'd', $pinned = true) {
  1618. global $CFG;
  1619. if (!empty($prefix)) {
  1620. $prefix .= '.';
  1621. }
  1622. $dir = $desc ? 'DESC' : 'ASC';
  1623. if ($pinned == true) {
  1624. $pinned = "{$prefix}pinned DESC,";
  1625. } else {
  1626. $pinned = '';
  1627. }
  1628. $sort = "{$prefix}timemodified";
  1629. if (!empty($CFG->forum_enabletimedposts)) {
  1630. $sort = "CASE WHEN {$compare} < {$prefix}timestart
  1631. THEN {$prefix}timestart
  1632. ELSE {$compare}
  1633. END";
  1634. }
  1635. return "$pinned $sort $dir";
  1636. }
  1637. /**
  1638. *
  1639. * @global object
  1640. * @global object
  1641. * @global object
  1642. * @uses CONTEXT_MODULE
  1643. * @uses VISIBLEGROUPS
  1644. * @param object $cm
  1645. * @return array
  1646. */
  1647. function forum_get_discussions_unread($cm) {
  1648. global $CFG, $DB, $USER;
  1649. $now = floor(time() / 60) * 60;
  1650. $cutoffdate = $now - ($CFG->forum_oldpostdays*24*60*60);
  1651. $params = array();
  1652. $groupmode = groups_get_activity_groupmode($cm);
  1653. $currentgroup = groups_get_activity_group($cm);
  1654. if ($groupmode) {
  1655. $modcontext = context_module::instance($cm->id);
  1656. if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $modcontext)) {
  1657. if ($currentgroup) {
  1658. $groupselect = "AND (d.groupid = :currentgroup OR d.groupid = -1)";
  1659. $params['currentgroup'] = $currentgroup;
  1660. } else {
  1661. $groupselect = "";
  1662. }
  1663. } else {
  1664. //separate groups without access all
  1665. if ($currentgroup) {
  1666. $groupselect = "AND (d.groupid = :currentgroup OR d.groupid = -1)";
  1667. $params['currentgroup'] = $currentgroup;
  1668. } else {
  1669. $groupselect = "AND d.groupid = -1";
  1670. }
  1671. }
  1672. } else {
  1673. $groupselect = "";
  1674. }
  1675. if (!empty($CFG->forum_enabletimedposts)) {
  1676. $timedsql = "AND d.timestart < :now1 AND (d.timeend = 0 OR d.timeend > :now2)";
  1677. $params['now1'] = $now;
  1678. $params['now2'] = $now;
  1679. } else {
  1680. $timedsql = "";
  1681. }
  1682. $sql = "SELECT d.id, COUNT(p.id) AS unread
  1683. FROM {forum_discussions} d
  1684. JOIN {forum_posts} p ON p.discussion = d.id
  1685. LEFT JOIN {forum_read} r ON (r.postid = p.id AND r.userid = $USER->id)
  1686. WHERE d.forum = {$cm->instance}
  1687. AND p.modified >= :cutoffdate AND r.id is NULL
  1688. $groupselect
  1689. $timedsql
  1690. GROUP BY d.id";
  1691. $params['cutoffdate'] = $cutoffdate;
  1692. if ($unreads = $DB->get_records_sql($sql, $params)) {
  1693. foreach ($unreads as $unread) {
  1694. $unreads[$unread->id] = $unread->unread;
  1695. }
  1696. return $unreads;
  1697. } else {
  1698. return array();
  1699. }
  1700. }
  1701. /**
  1702. * @global object
  1703. * @global object
  1704. * @global object
  1705. * @uses CONEXT_MODULE
  1706. * @uses VISIBLEGROUPS
  1707. * @param object $cm
  1708. * @return array
  1709. */
  1710. function forum_get_discussions_count($cm) {
  1711. global $CFG, $DB, $USER;
  1712. $now = floor(time() / 60) * 60;
  1713. $params = array($cm->instance);
  1714. $groupmode = groups_get_activity_groupmode($cm);
  1715. $currentgroup = groups_get_activity_group($cm);
  1716. if ($groupmode) {
  1717. $modcontext = context_module::instance($cm->id);
  1718. if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $modcontext)) {
  1719. if ($currentgroup) {
  1720. $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
  1721. $params[] = $currentgroup;
  1722. } else {
  1723. $groupselect = "";
  1724. }
  1725. } else {
  1726. //seprate groups without access all
  1727. if ($currentgroup) {
  1728. $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
  1729. $params[] = $currentgroup;
  1730. } else {
  1731. $groupselect = "AND d.groupid = -1";
  1732. }
  1733. }
  1734. } else {
  1735. $groupselect = "";
  1736. }
  1737. $timelimit = "";
  1738. if (!empty($CFG->forum_enabletimedposts)) {
  1739. $modcontext = context_module::instance($cm->id);
  1740. if (!has_capability('mod/forum:viewhiddentimedposts', $modcontext)) {
  1741. $timelimit = " AND ((d.timestart <= ? AND (d.timeend = 0 OR d.timeend > ?))";
  1742. $params[] = $now;
  1743. $params[] = $now;
  1744. if (isloggedin()) {
  1745. $timelimit .= " OR d.userid = ?";
  1746. $params[] = $USER->id;
  1747. }
  1748. $timelimit .= ")";
  1749. }
  1750. }
  1751. $sql = "SELECT COUNT(d.id)
  1752. FROM {forum_discussions} d
  1753. JOIN {forum_posts} p ON p.discussion = d.id
  1754. WHERE d.forum = ? AND p.parent = 0
  1755. $groupselect $timelimit";
  1756. return $DB->get_field_sql($sql, $params);
  1757. }
  1758. // OTHER FUNCTIONS ///////////////////////////////////////////////////////////
  1759. /**
  1760. * @global object
  1761. * @global object
  1762. * @param int $courseid
  1763. * @param string $type
  1764. */
  1765. function forum_get_course_forum($courseid, $type) {
  1766. // How to set up special 1-per-course forums
  1767. global $CFG, $DB, $OUTPUT, $USER;
  1768. if ($forums = $DB->get_records_select("forum", "course = ? AND type = ?", array($courseid, $type), "id ASC")) {
  1769. // There should always only be ONE, but with the right combination of
  1770. // errors there might be more. In this case, just return the oldest one (lowest ID).
  1771. foreach ($forums as $forum) {
  1772. return $forum; // ie the first one
  1773. }
  1774. }
  1775. // Doesn't exist, so create one now.
  1776. $forum = new stdClass();
  1777. $forum->course = $courseid;
  1778. $forum->type = "$type";
  1779. if (!empty($USER->htmleditor)) {
  1780. $forum->introformat = $USER->htmleditor;
  1781. }
  1782. switch ($forum->type) {
  1783. case "news":
  1784. $forum->name = get_string("namenews", "forum");
  1785. $forum->intro = get_string("intronews", "forum");
  1786. $forum->introformat = FORMAT_HTML;
  1787. $forum->forcesubscribe = FORUM_FORCESUBSCRIBE;
  1788. $forum->assessed = 0;
  1789. if ($courseid == SITEID) {
  1790. $forum->name = get_string("sitenews");
  1791. $forum->forcesubscribe = 0;
  1792. }
  1793. break;
  1794. case "social":
  1795. $forum->name = get_string("namesocial", "forum");
  1796. $forum->intro = get_string("introsocial", "forum");
  1797. $forum->introformat = FORMAT_HTML;
  1798. $forum->assessed = 0;
  1799. $forum->forcesubscribe = 0;
  1800. break;
  1801. case "blog":
  1802. $forum->name = get_string('blogforum', 'forum');
  1803. $forum->intro = get_string('introblog', 'forum');
  1804. $forum->introformat = FORMAT_HTML;
  1805. $forum->assessed = 0;
  1806. $forum->forcesubscribe = 0;
  1807. break;
  1808. default:
  1809. echo $OUTPUT->notification("That forum type doesn't exist!");
  1810. return false;
  1811. break;
  1812. }
  1813. $forum->timemodified = time();
  1814. $forum->id = $DB->insert_record("forum", $forum);
  1815. if (! $module = $DB->get_record("modules", array("name" => "forum"))) {
  1816. echo $OUTPUT->notification("Could not find forum module!!");
  1817. return false;
  1818. }
  1819. $mod = new stdClass();
  1820. $mod->course = $courseid;
  1821. $mod->module = $module->id;
  1822. $mod->instance = $forum->id;
  1823. $mod->section = 0;
  1824. include_once("$CFG->dirroot/course/lib.php");
  1825. if (! $mod->coursemodule = add_course_module($mod) ) {
  1826. echo $OUTPUT->notification("Could not add a new course module to the course '" . $courseid . "'");
  1827. return false;
  1828. }
  1829. $sectionid = course_add_cm_to_section($courseid, $mod->coursemodule, 0);
  1830. return $DB->get_record("forum", array("id" => "$forum->id"));
  1831. }
  1832. /**
  1833. * Return rating related permissions
  1834. *
  1835. * @param string $options the context id
  1836. * @return array an associative array of the user's rating permissions
  1837. */
  1838. function forum_rating_permissions($contextid, $component, $ratingarea) {
  1839. $context = context::instance_by_id($contextid, MUST_EXIST);
  1840. if ($component != 'mod_forum' || $ratingarea != 'post') {
  1841. // We don't know about this component/ratingarea so just return null to get the
  1842. // default restrictive permissions.
  1843. return null;
  1844. }
  1845. return array(
  1846. 'view' => has_capability('mod/forum:viewrating', $context),
  1847. 'viewany' => has_capability('mod/forum:viewanyrating', $context),
  1848. 'viewall' => has_capability('mod/forum:viewallratings', $context),
  1849. 'rate' => has_capability('mod/forum:rate', $context)
  1850. );
  1851. }
  1852. /**
  1853. * Validates a submitted rating
  1854. * @param array $params submitted data
  1855. * context => object the context in which the rated items exists [required]
  1856. * component => The component for this module - should always be mod_forum [required]
  1857. * ratingarea => object the context in which the rated items exists [required]
  1858. *
  1859. * itemid => int the ID of the object being rated [required]
  1860. * scaleid => int the scale from which the user can select a rating. Used for bounds checking. [required]
  1861. * rating => int the submitted rating [required]
  1862. * rateduserid => int the id of the user whose items have been rated. NOT the user who submitted the ratings. 0 to update all. [required]
  1863. * aggregation => int the aggregation method to apply when calculating grades ie RATING_AGGREGATE_AVERAGE [required]
  1864. * @return boolean true if the rating is valid. Will throw rating_exception if not
  1865. */
  1866. function forum_rating_validate($params) {
  1867. global $DB, $USER;
  1868. // Check the component is mod_forum
  1869. if ($params['component'] != 'mod_forum') {
  1870. throw new rating_exception('invalidcomponent');
  1871. }
  1872. // Check the ratingarea is post (the only rating area in forum)
  1873. if ($params['ratingarea'] != 'post') {
  1874. throw new rating_exception('invalidratingarea');
  1875. }
  1876. // Check the rateduserid is not the current user .. you can't rate your own posts
  1877. if ($params['rateduserid'] == $USER->id) {
  1878. throw new rating_exception('nopermissiontorate');
  1879. }
  1880. // Fetch all the related records ... we need to do this anyway to call forum_user_can_see_post
  1881. $post = $DB->get_record('forum_posts', array('id' => $params['itemid'], 'userid' => $params['rateduserid']), '*', MUST_EXIST);
  1882. $discussion = $DB->get_record('forum_discussions', array('id' => $post->discussion), '*', MUST_EXIST);
  1883. $forum = $DB->get_record('forum', array('id' => $discussion->forum), '*', MUST_EXIST);
  1884. $course = $DB->get_record('course', array('id' => $forum->course), '*', MUST_EXIST);
  1885. $cm = get_coursemodule_from_instance('forum', $forum->id, $course->id , false, MUST_EXIST);
  1886. $context = context_module::instance($cm->id);
  1887. // Make sure the context provided is the context of the forum
  1888. if ($context->id != $params['context']->id) {
  1889. throw new rating_exception('invalidcontext');
  1890. }
  1891. if ($forum->scale != $params['scaleid']) {
  1892. //the scale being submitted doesnt match the one in the database
  1893. throw new rating_exception('invalidscaleid');
  1894. }
  1895. // check the item we're rating was created in the assessable time window
  1896. if (!empty($forum->assesstimestart) && !empty($forum->assesstimefinish)) {
  1897. if ($post->created < $forum->assesstimestart || $post->created > $forum->assesstimefinish) {
  1898. throw new rating_exception('notavailable');
  1899. }
  1900. }
  1901. //check that the submitted rating is valid for the scale
  1902. // lower limit
  1903. if ($params['rating'] < 0 && $params['rating'] != RATING_UNSET_RATING) {
  1904. throw new rating_exception('invalidnum');
  1905. }
  1906. // upper limit
  1907. if ($forum->scale < 0) {
  1908. //its a custom scale
  1909. $scalerecord = $DB->get_record('scale', array('id' => -$forum->scale));
  1910. if ($scalerecord) {
  1911. $scalearray = explode(',', $scalerecord->scale);
  1912. if ($params['rating'] > count($scalearray)) {
  1913. throw new rating_exception('invalidnum');
  1914. }
  1915. } else {
  1916. throw new rating_exception('invalidscaleid');
  1917. }
  1918. } else if ($params['rating'] > $forum->scale) {
  1919. //if its numeric and submitted rating is above maximum
  1920. throw new rating_exception('invalidnum');
  1921. }
  1922. // Make sure groups allow this user to see the item they're rating
  1923. if ($discussion->groupid > 0 and $groupmode = groups_get_activity_groupmode($cm, $course)) { // Groups are being used
  1924. if (!groups_group_exists($discussion->groupid)) { // Can't find group
  1925. throw new rating_exception('cannotfindgroup');//something is wrong
  1926. }
  1927. if (!groups_is_member($discussion->groupid) and !has_capability('moodle/site:accessallgroups', $context)) {
  1928. // do not allow rating of posts from other groups when in SEPARATEGROUPS or VISIBLEGROUPS
  1929. throw new rating_exception('notmemberofgroup');
  1930. }
  1931. }
  1932. // perform some final capability checks
  1933. if (!forum_user_can_see_post($forum, $discussion, $post, $USER, $cm)) {
  1934. throw new rating_exception('nopermissiontorate');
  1935. }
  1936. return true;
  1937. }
  1938. /**
  1939. * Can the current user see ratings for a given itemid?
  1940. *
  1941. * @param array $params submitted data
  1942. * contextid => int contextid [required]
  1943. * component => The component for this module - should always be mod_forum [required]
  1944. * ratingarea => object the context in which the rated items exists [required]
  1945. * itemid => int the ID of the object being rated [required]
  1946. * scaleid => int scale id [optional]
  1947. * @return bool
  1948. * @throws coding_exception
  1949. * @throws rating_exception
  1950. */
  1951. function mod_forum_rating_can_see_item_ratings($params) {
  1952. global $DB, $USER;
  1953. // Check the component is mod_forum.
  1954. if (!isset($params['component']) || $params['component'] != 'mod_forum') {
  1955. throw new rating_exception('invalidcomponent');
  1956. }
  1957. // Check the ratingarea is post (the only rating area in forum).
  1958. if (!isset($params['ratingarea']) || $params['ratingarea'] != 'post') {
  1959. throw new rating_exception('invalidratingarea');
  1960. }
  1961. if (!isset($params['itemid'])) {
  1962. throw new rating_exception('invaliditemid');
  1963. }
  1964. $post = $DB->get_record('forum_posts', array('id' => $params['itemid']), '*', MUST_EXIST);
  1965. $discussion = $DB->get_record('forum_discussions', array('id' => $post->discussion), '*', MUST_EXIST);
  1966. $forum = $DB->get_record('forum', array('id' => $discussion->forum), '*', MUST_EXIST);
  1967. $course = $DB->get_record('course', array('id' => $forum->course), '*', MUST_EXIST);
  1968. $cm = get_coursemodule_from_instance('forum', $forum->id, $course->id , false, MUST_EXIST);
  1969. // Perform some final capability checks.
  1970. if (!forum_user_can_see_post($forum, $discussion, $post, $USER, $cm)) {
  1971. return false;
  1972. }
  1973. return true;
  1974. }
  1975. /**
  1976. * This function prints the overview of a discussion in the forum listing.
  1977. * It needs some discussion information and some post information, these
  1978. * happen to be combined for efficiency in the $post parameter by the function
  1979. * that calls this one: forum_print_latest_discussions()
  1980. *
  1981. * @global object
  1982. * @global object
  1983. * @param object $post The post object (passed by reference for speed).
  1984. * @param object $forum The forum object.
  1985. * @param int $group Current group.
  1986. * @param string $datestring Format to use for the dates.
  1987. * @param boolean $cantrack Is tracking enabled for this forum.
  1988. * @param boolean $forumtracked Is the user tracking this forum.
  1989. * @param boolean $canviewparticipants True if user has the viewparticipants permission for this course
  1990. * @param boolean $canviewhiddentimedposts True if user has the viewhiddentimedposts permission for this forum
  1991. */
  1992. function forum_print_discussion_header(&$post, $forum, $group = -1, $datestring = "",
  1993. $cantrack = true, $forumtracked = true, $canviewparticipants = true, $modcontext = null,
  1994. $canviewhiddentimedposts = false) {
  1995. global $COURSE, $USER, $CFG, $OUTPUT, $PAGE;
  1996. static $rowcount;
  1997. static $strmarkalldread;
  1998. if (empty($modcontext)) {
  1999. if (!$cm = get_coursemodule_from_instance('forum', $forum->id, $forum->course)) {
  2000. print_error('invalidcoursemodule');
  2001. }
  2002. $modcontext = context_module::instance($cm->id);
  2003. }
  2004. if (!isset($rowcount)) {
  2005. $rowcount = 0;
  2006. $strmarkalldread = get_string('markalldread', 'forum');
  2007. } else {
  2008. $rowcount = ($rowcount + 1) % 2;
  2009. }
  2010. $post->subject = format_string($post->subject,true);
  2011. $canviewfullnames = has_capability('moodle/site:viewfullnames', $modcontext);
  2012. $timeddiscussion = !empty($CFG->forum_enabletimedposts) && ($post->timestart || $post->timeend);
  2013. $timedoutsidewindow = '';
  2014. if ($timeddiscussion && ($post->timestart > time() || ($post->timeend != 0 && $post->timeend < time()))) {
  2015. $timedoutsidewindow = ' dimmed_text';
  2016. }
  2017. echo "\n\n";
  2018. echo '<tr class="discussion r'.$rowcount.$timedoutsidewindow.'">';
  2019. $topicclass = 'topic starter';
  2020. if (FORUM_DISCUSSION_PINNED == $post->pinned) {
  2021. $topicclass .= ' pinned';
  2022. }
  2023. echo '<td class="'.$topicclass.'">';
  2024. if (FORUM_DISCUSSION_PINNED == $post->pinned) {
  2025. echo $OUTPUT->pix_icon('i/pinned', get_string('discussionpinned', 'forum'), 'mod_forum');
  2026. }
  2027. $canalwaysseetimedpost = $USER->id == $post->userid || $canviewhiddentimedposts;
  2028. if ($timeddiscussion && $canalwaysseetimedpost) {
  2029. echo $PAGE->get_renderer('mod_forum')->timed_discussion_tooltip($post, empty($timedoutsidewindow));
  2030. }
  2031. echo '<a href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$post->discussion.'">'.$post->subject.'</a>';
  2032. echo "</td>\n";
  2033. // Picture
  2034. $postuser = new stdClass();
  2035. $postuserfields = explode(',', implode(',', \core_user\fields::get_picture_fields()));
  2036. $postuser = username_load_fields_from_object($postuser, $post, null, $postuserfields);
  2037. $postuser->id = $post->userid;
  2038. echo '<td class="author">';
  2039. echo '<div class="media">';
  2040. echo '<span class="float-left">';
  2041. echo $OUTPUT->user_picture($postuser, array('courseid'=>$forum->course));
  2042. echo '</span>';
  2043. // User name
  2044. echo '<div class="media-body">';
  2045. $fullname = fullname($postuser, $canviewfullnames);
  2046. echo '<a href="'.$CFG->wwwroot.'/user/view.php?id='.$post->userid.'&amp;course='.$forum->course.'">'.$fullname.'</a>';
  2047. echo '</div>';
  2048. echo '</div>';
  2049. echo "</td>\n";
  2050. // Group picture
  2051. if ($group !== -1) { // Groups are active - group is a group data object or NULL
  2052. echo '<td class="picture group">';
  2053. if (!empty($group->picture)) {
  2054. if ($canviewparticipants && $COURSE->groupmode) {
  2055. $picturelink = true;
  2056. } else {
  2057. $picturelink = false;
  2058. }
  2059. print_group_picture($group, $forum->course, false, false, $picturelink);
  2060. } else if (isset($group->id)) {
  2061. if ($canviewparticipants && $COURSE->groupmode) {
  2062. echo '<a href="'.$CFG->wwwroot.'/user/index.php?id='.$forum->course.'&amp;group='.$group->id.'">'.$group->name.'</a>';
  2063. } else {
  2064. echo $group->name;
  2065. }
  2066. }
  2067. echo "</td>\n";
  2068. }
  2069. if (has_capability('mod/forum:viewdiscussion', $modcontext)) { // Show the column with replies
  2070. echo '<td class="replies">';
  2071. echo '<a href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$post->discussion.'">';
  2072. echo $post->replies.'</a>';
  2073. echo "</td>\n";
  2074. if ($cantrack) {
  2075. echo '<td class="replies">';
  2076. if ($forumtracked) {
  2077. if ($post->unread > 0) {
  2078. echo '<span class="unread">';
  2079. echo '<a href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$post->discussion.'#unread">';
  2080. echo $post->unread;
  2081. echo '</a>';
  2082. echo '<a title="'.$strmarkalldread.'" href="'.$CFG->wwwroot.'/mod/forum/markposts.php?f='.
  2083. $forum->id.'&amp;d='.$post->discussion.'&amp;mark=read&amp;return=/mod/forum/view.php&amp;sesskey=' .
  2084. sesskey() . '">' . $OUTPUT->pix_icon('t/markasread', $strmarkalldread) . '</a>';
  2085. echo '</span>';
  2086. } else {
  2087. echo '<span class="read">';
  2088. echo $post->unread;
  2089. echo '</span>';
  2090. }
  2091. } else {
  2092. echo '<span class="read">';
  2093. echo '-';
  2094. echo '</span>';
  2095. }
  2096. echo "</td>\n";
  2097. }
  2098. }
  2099. echo '<td class="lastpost">';
  2100. $usedate = (empty($post->timemodified)) ? $post->created : $post->timemodified;
  2101. $parenturl = '';
  2102. $usermodified = new stdClass();
  2103. $usermodified->id = $post->usermodified;
  2104. $usermodified = username_load_fields_from_object($usermodified, $post, 'um');
  2105. // In QA forums we check that the user can view participants.
  2106. if ($forum->type !== 'qanda' || $canviewparticipants) {
  2107. echo '<a href="'.$CFG->wwwroot.'/user/view.php?id='.$post->usermodified.'&amp;course='.$forum->course.'">'.
  2108. fullname($usermodified, $canviewfullnames).'</a><br />';
  2109. $parenturl = (empty($post->lastpostid)) ? '' : '&amp;parent='.$post->lastpostid;
  2110. }
  2111. echo '<a href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$post->discussion.$parenturl.'">'.
  2112. userdate_htmltime($usedate, $datestring).'</a>';
  2113. echo "</td>\n";
  2114. // is_guest should be used here as this also checks whether the user is a guest in the current course.
  2115. // Guests and visitors cannot subscribe - only enrolled users.
  2116. if ((!is_guest($modcontext, $USER) && isloggedin()) && has_capability('mod/forum:viewdiscussion', $modcontext)) {
  2117. // Discussion subscription.
  2118. if (\mod_forum\subscriptions::is_subscribable($forum)) {
  2119. echo '<td class="discussionsubscription">';
  2120. echo forum_get_discussion_subscription_icon($forum, $post->discussion);
  2121. echo '</td>';
  2122. }
  2123. }
  2124. echo "</tr>\n\n";
  2125. }
  2126. /**
  2127. * Return the markup for the discussion subscription toggling icon.
  2128. *
  2129. * @param stdClass $forum The forum object.
  2130. * @param int $discussionid The discussion to create an icon for.
  2131. * @return string The generated markup.
  2132. */
  2133. function forum_get_discussion_subscription_icon($forum, $discussionid, $returnurl = null, $includetext = false) {
  2134. global $USER, $OUTPUT, $PAGE;
  2135. if ($returnurl === null && $PAGE->url) {
  2136. $returnurl = $PAGE->url->out();
  2137. }
  2138. $o = '';
  2139. $subscriptionstatus = \mod_forum\subscriptions::is_subscribed($USER->id, $forum, $discussionid);
  2140. $subscriptionlink = new moodle_url('/mod/forum/subscribe.php', array(
  2141. 'sesskey' => sesskey(),
  2142. 'id' => $forum->id,
  2143. 'd' => $discussionid,
  2144. 'returnurl' => $returnurl,
  2145. ));
  2146. if ($includetext) {
  2147. $o .= $subscriptionstatus ? get_string('subscribed', 'mod_forum') : get_string('notsubscribed', 'mod_forum');
  2148. }
  2149. if ($subscriptionstatus) {
  2150. $output = $OUTPUT->pix_icon('t/subscribed', get_string('clicktounsubscribe', 'forum'), 'mod_forum');
  2151. if ($includetext) {
  2152. $output .= get_string('subscribed', 'mod_forum');
  2153. }
  2154. return html_writer::link($subscriptionlink, $output, array(
  2155. 'title' => get_string('clicktounsubscribe', 'forum'),
  2156. 'class' => 'discussiontoggle btn btn-link',
  2157. 'data-forumid' => $forum->id,
  2158. 'data-discussionid' => $discussionid,
  2159. 'data-includetext' => $includetext,
  2160. ));
  2161. } else {
  2162. $output = $OUTPUT->pix_icon('t/unsubscribed', get_string('clicktosubscribe', 'forum'), 'mod_forum');
  2163. if ($includetext) {
  2164. $output .= get_string('notsubscribed', 'mod_forum');
  2165. }
  2166. return html_writer::link($subscriptionlink, $output, array(
  2167. 'title' => get_string('clicktosubscribe', 'forum'),
  2168. 'class' => 'discussiontoggle btn btn-link',
  2169. 'data-forumid' => $forum->id,
  2170. 'data-discussionid' => $discussionid,
  2171. 'data-includetext' => $includetext,
  2172. ));
  2173. }
  2174. }
  2175. /**
  2176. * Return a pair of spans containing classes to allow the subscribe and
  2177. * unsubscribe icons to be pre-loaded by a browser.
  2178. *
  2179. * @return string The generated markup
  2180. */
  2181. function forum_get_discussion_subscription_icon_preloaders() {
  2182. $o = '';
  2183. $o .= html_writer::span('&nbsp;', 'preload-subscribe');
  2184. $o .= html_writer::span('&nbsp;', 'preload-unsubscribe');
  2185. return $o;
  2186. }
  2187. /**
  2188. * Print the drop down that allows the user to select how they want to have
  2189. * the discussion displayed.
  2190. *
  2191. * @param int $id forum id if $forumtype is 'single',
  2192. * discussion id for any other forum type
  2193. * @param mixed $mode forum layout mode
  2194. * @param string $forumtype optional
  2195. */
  2196. function forum_print_mode_form($id, $mode, $forumtype='') {
  2197. global $OUTPUT;
  2198. $useexperimentalui = get_user_preferences('forum_useexperimentalui', false);
  2199. if ($forumtype == 'single') {
  2200. $select = new single_select(
  2201. new moodle_url("/mod/forum/view.php",
  2202. array('f' => $id)),
  2203. 'mode',
  2204. forum_get_layout_modes($useexperimentalui),
  2205. $mode,
  2206. null,
  2207. "mode"
  2208. );
  2209. $select->set_label(get_string('displaymode', 'forum'), array('class' => 'accesshide'));
  2210. $select->class = "forummode";
  2211. } else {
  2212. $select = new single_select(
  2213. new moodle_url("/mod/forum/discuss.php",
  2214. array('d' => $id)),
  2215. 'mode',
  2216. forum_get_layout_modes($useexperimentalui),
  2217. $mode,
  2218. null,
  2219. "mode"
  2220. );
  2221. $select->set_label(get_string('displaymode', 'forum'), array('class' => 'accesshide'));
  2222. }
  2223. echo $OUTPUT->render($select);
  2224. }
  2225. /**
  2226. * @global object
  2227. * @param object $course
  2228. * @param string $search
  2229. * @return string
  2230. */
  2231. function forum_search_form($course, $search='') {
  2232. global $CFG, $PAGE;
  2233. $forumsearch = new \mod_forum\output\quick_search_form($course->id, $search);
  2234. $output = $PAGE->get_renderer('mod_forum');
  2235. return $output->render($forumsearch);
  2236. }
  2237. /**
  2238. * @global object
  2239. * @global object
  2240. */
  2241. function forum_set_return() {
  2242. global $CFG, $SESSION;
  2243. if (! isset($SESSION->fromdiscussion)) {
  2244. $referer = get_local_referer(false);
  2245. // If the referer is NOT a login screen then save it.
  2246. if (! strncasecmp("$CFG->wwwroot/login", $referer, 300)) {
  2247. $SESSION->fromdiscussion = $referer;
  2248. }
  2249. }
  2250. }
  2251. /**
  2252. * @global object
  2253. * @param string|\moodle_url $default
  2254. * @return string
  2255. */
  2256. function forum_go_back_to($default) {
  2257. global $SESSION;
  2258. if (!empty($SESSION->fromdiscussion)) {
  2259. $returnto = $SESSION->fromdiscussion;
  2260. unset($SESSION->fromdiscussion);
  2261. return $returnto;
  2262. } else {
  2263. return $default;
  2264. }
  2265. }
  2266. /**
  2267. * Given a discussion object that is being moved to $forumto,
  2268. * this function checks all posts in that discussion
  2269. * for attachments, and if any are found, these are
  2270. * moved to the new forum directory.
  2271. *
  2272. * @global object
  2273. * @param object $discussion
  2274. * @param int $forumfrom source forum id
  2275. * @param int $forumto target forum id
  2276. * @return bool success
  2277. */
  2278. function forum_move_attachments($discussion, $forumfrom, $forumto) {
  2279. global $DB;
  2280. $fs = get_file_storage();
  2281. $newcm = get_coursemodule_from_instance('forum', $forumto);
  2282. $oldcm = get_coursemodule_from_instance('forum', $forumfrom);
  2283. $newcontext = context_module::instance($newcm->id);
  2284. $oldcontext = context_module::instance($oldcm->id);
  2285. // loop through all posts, better not use attachment flag ;-)
  2286. if ($posts = $DB->get_records('forum_posts', array('discussion'=>$discussion->id), '', 'id, attachment')) {
  2287. foreach ($posts as $post) {
  2288. $fs->move_area_files_to_new_context($oldcontext->id,
  2289. $newcontext->id, 'mod_forum', 'post', $post->id);
  2290. $attachmentsmoved = $fs->move_area_files_to_new_context($oldcontext->id,
  2291. $newcontext->id, 'mod_forum', 'attachment', $post->id);
  2292. if ($attachmentsmoved > 0 && $post->attachment != '1') {
  2293. // Weird - let's fix it
  2294. $post->attachment = '1';
  2295. $DB->update_record('forum_posts', $post);
  2296. } else if ($attachmentsmoved == 0 && $post->attachment != '') {
  2297. // Weird - let's fix it
  2298. $post->attachment = '';
  2299. $DB->update_record('forum_posts', $post);
  2300. }
  2301. }
  2302. }
  2303. return true;
  2304. }
  2305. /**
  2306. * Returns attachments as formated text/html optionally with separate images
  2307. *
  2308. * @global object
  2309. * @global object
  2310. * @global object
  2311. * @param object $post
  2312. * @param object $cm
  2313. * @param string $type html/text/separateimages
  2314. * @return mixed string or array of (html text withouth images and image HTML)
  2315. */
  2316. function forum_print_attachments($post, $cm, $type) {
  2317. global $CFG, $DB, $USER, $OUTPUT;
  2318. if (empty($post->attachment)) {
  2319. return $type !== 'separateimages' ? '' : array('', '');
  2320. }
  2321. if (!in_array($type, array('separateimages', 'html', 'text'))) {
  2322. return $type !== 'separateimages' ? '' : array('', '');
  2323. }
  2324. if (!$context = context_module::instance($cm->id)) {
  2325. return $type !== 'separateimages' ? '' : array('', '');
  2326. }
  2327. $strattachment = get_string('attachment', 'forum');
  2328. $fs = get_file_storage();
  2329. $imagereturn = '';
  2330. $output = '';
  2331. $canexport = !empty($CFG->enableportfolios) && (has_capability('mod/forum:exportpost', $context) || ($post->userid == $USER->id && has_capability('mod/forum:exportownpost', $context)));
  2332. if ($canexport) {
  2333. require_once($CFG->libdir.'/portfoliolib.php');
  2334. }
  2335. // We retrieve all files according to the time that they were created. In the case that several files were uploaded
  2336. // at the sametime (e.g. in the case of drag/drop upload) we revert to using the filename.
  2337. $files = $fs->get_area_files($context->id, 'mod_forum', 'attachment', $post->id, "filename", false);
  2338. if ($files) {
  2339. if ($canexport) {
  2340. $button = new portfolio_add_button();
  2341. }
  2342. foreach ($files as $file) {
  2343. $filename = $file->get_filename();
  2344. $mimetype = $file->get_mimetype();
  2345. $iconimage = $OUTPUT->pix_icon(file_file_icon($file), get_mimetype_description($file), 'moodle', array('class' => 'icon'));
  2346. $path = file_encode_url($CFG->wwwroot.'/pluginfile.php', '/'.$context->id.'/mod_forum/attachment/'.$post->id.'/'.$filename);
  2347. if ($type == 'html') {
  2348. $output .= "<a href=\"$path\">$iconimage</a> ";
  2349. $output .= "<a href=\"$path\">".s($filename)."</a>";
  2350. if ($canexport) {
  2351. $button->set_callback_options('forum_portfolio_caller', array('postid' => $post->id, 'attachment' => $file->get_id()), 'mod_forum');
  2352. $button->set_format_by_file($file);
  2353. $output .= $button->to_html(PORTFOLIO_ADD_ICON_LINK);
  2354. }
  2355. $output .= "<br />";
  2356. } else if ($type == 'text') {
  2357. $output .= "$strattachment ".s($filename).":\n$path\n";
  2358. } else { //'returnimages'
  2359. if (in_array($mimetype, array('image/gif', 'image/jpeg', 'image/png'))) {
  2360. // Image attachments don't get printed as links
  2361. $imagereturn .= "<br /><img src=\"$path\" alt=\"\" />";
  2362. if ($canexport) {
  2363. $button->set_callback_options('forum_portfolio_caller', array('postid' => $post->id, 'attachment' => $file->get_id()), 'mod_forum');
  2364. $button->set_format_by_file($file);
  2365. $imagereturn .= $button->to_html(PORTFOLIO_ADD_ICON_LINK);
  2366. }
  2367. } else {
  2368. $output .= "<a href=\"$path\">$iconimage</a> ";
  2369. $output .= format_text("<a href=\"$path\">".s($filename)."</a>", FORMAT_HTML, array('context'=>$context));
  2370. if ($canexport) {
  2371. $button->set_callback_options('forum_portfolio_caller', array('postid' => $post->id, 'attachment' => $file->get_id()), 'mod_forum');
  2372. $button->set_format_by_file($file);
  2373. $output .= $button->to_html(PORTFOLIO_ADD_ICON_LINK);
  2374. }
  2375. $output .= '<br />';
  2376. }
  2377. }
  2378. if (!empty($CFG->enableplagiarism)) {
  2379. require_once($CFG->libdir.'/plagiarismlib.php');
  2380. $output .= plagiarism_get_links(array('userid' => $post->userid,
  2381. 'file' => $file,
  2382. 'cmid' => $cm->id,
  2383. 'course' => $cm->course,
  2384. 'forum' => $cm->instance));
  2385. $output .= '<br />';
  2386. }
  2387. }
  2388. }
  2389. if ($type !== 'separateimages') {
  2390. return $output;
  2391. } else {
  2392. return array($output, $imagereturn);
  2393. }
  2394. }
  2395. ////////////////////////////////////////////////////////////////////////////////
  2396. // File API //
  2397. ////////////////////////////////////////////////////////////////////////////////
  2398. /**
  2399. * Lists all browsable file areas
  2400. *
  2401. * @package mod_forum
  2402. * @category files
  2403. * @param stdClass $course course object
  2404. * @param stdClass $cm course module object
  2405. * @param stdClass $context context object
  2406. * @return array
  2407. */
  2408. function forum_get_file_areas($course, $cm, $context) {
  2409. return array(
  2410. 'attachment' => get_string('areaattachment', 'mod_forum'),
  2411. 'post' => get_string('areapost', 'mod_forum'),
  2412. );
  2413. }
  2414. /**
  2415. * File browsing support for forum module.
  2416. *
  2417. * @package mod_forum
  2418. * @category files
  2419. * @param stdClass $browser file browser object
  2420. * @param stdClass $areas file areas
  2421. * @param stdClass $course course object
  2422. * @param stdClass $cm course module
  2423. * @param stdClass $context context module
  2424. * @param string $filearea file area
  2425. * @param int $itemid item ID
  2426. * @param string $filepath file path
  2427. * @param string $filename file name
  2428. * @return file_info instance or null if not found
  2429. */
  2430. function forum_get_file_info($browser, $areas, $course, $cm, $context, $filearea, $itemid, $filepath, $filename) {
  2431. global $CFG, $DB, $USER;
  2432. if ($context->contextlevel != CONTEXT_MODULE) {
  2433. return null;
  2434. }
  2435. // filearea must contain a real area
  2436. if (!isset($areas[$filearea])) {
  2437. return null;
  2438. }
  2439. // Note that forum_user_can_see_post() additionally allows access for parent roles
  2440. // and it explicitly checks qanda forum type, too. One day, when we stop requiring
  2441. // course:managefiles, we will need to extend this.
  2442. if (!has_capability('mod/forum:viewdiscussion', $context)) {
  2443. return null;
  2444. }
  2445. if (is_null($itemid)) {
  2446. require_once($CFG->dirroot.'/mod/forum/locallib.php');
  2447. return new forum_file_info_container($browser, $course, $cm, $context, $areas, $filearea);
  2448. }
  2449. static $cached = array();
  2450. // $cached will store last retrieved post, discussion and forum. To make sure that the cache
  2451. // is cleared between unit tests we check if this is the same session
  2452. if (!isset($cached['sesskey']) || $cached['sesskey'] != sesskey()) {
  2453. $cached = array('sesskey' => sesskey());
  2454. }
  2455. if (isset($cached['post']) && $cached['post']->id == $itemid) {
  2456. $post = $cached['post'];
  2457. } else if ($post = $DB->get_record('forum_posts', array('id' => $itemid))) {
  2458. $cached['post'] = $post;
  2459. } else {
  2460. return null;
  2461. }
  2462. if (isset($cached['discussion']) && $cached['discussion']->id == $post->discussion) {
  2463. $discussion = $cached['discussion'];
  2464. } else if ($discussion = $DB->get_record('forum_discussions', array('id' => $post->discussion))) {
  2465. $cached['discussion'] = $discussion;
  2466. } else {
  2467. return null;
  2468. }
  2469. if (isset($cached['forum']) && $cached['forum']->id == $cm->instance) {
  2470. $forum = $cached['forum'];
  2471. } else if ($forum = $DB->get_record('forum', array('id' => $cm->instance))) {
  2472. $cached['forum'] = $forum;
  2473. } else {
  2474. return null;
  2475. }
  2476. $fs = get_file_storage();
  2477. $filepath = is_null($filepath) ? '/' : $filepath;
  2478. $filename = is_null($filename) ? '.' : $filename;
  2479. if (!($storedfile = $fs->get_file($context->id, 'mod_forum', $filearea, $itemid, $filepath, $filename))) {
  2480. return null;
  2481. }
  2482. // Checks to see if the user can manage files or is the owner.
  2483. // TODO MDL-33805 - Do not use userid here and move the capability check above.
  2484. if (!has_capability('moodle/course:managefiles', $context) && $storedfile->get_userid() != $USER->id) {
  2485. return null;
  2486. }
  2487. // Make sure groups allow this user to see this file
  2488. if ($discussion->groupid > 0 && !has_capability('moodle/site:accessallgroups', $context)) {
  2489. $groupmode = groups_get_activity_groupmode($cm, $course);
  2490. if ($groupmode == SEPARATEGROUPS && !groups_is_member($discussion->groupid)) {
  2491. return null;
  2492. }
  2493. }
  2494. // Make sure we're allowed to see it...
  2495. if (!forum_user_can_see_post($forum, $discussion, $post, NULL, $cm)) {
  2496. return null;
  2497. }
  2498. $urlbase = $CFG->wwwroot.'/pluginfile.php';
  2499. return new file_info_stored($browser, $context, $storedfile, $urlbase, $itemid, true, true, false, false);
  2500. }
  2501. /**
  2502. * Serves the forum attachments. Implements needed access control ;-)
  2503. *
  2504. * @package mod_forum
  2505. * @category files
  2506. * @param stdClass $course course object
  2507. * @param stdClass $cm course module object
  2508. * @param stdClass $context context object
  2509. * @param string $filearea file area
  2510. * @param array $args extra arguments
  2511. * @param bool $forcedownload whether or not force download
  2512. * @param array $options additional options affecting the file serving
  2513. * @return bool false if file not found, does not return if found - justsend the file
  2514. */
  2515. function forum_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) {
  2516. global $CFG, $DB;
  2517. if ($context->contextlevel != CONTEXT_MODULE) {
  2518. return false;
  2519. }
  2520. require_course_login($course, true, $cm);
  2521. $areas = forum_get_file_areas($course, $cm, $context);
  2522. // filearea must contain a real area
  2523. if (!isset($areas[$filearea])) {
  2524. return false;
  2525. }
  2526. $postid = (int)array_shift($args);
  2527. if (!$post = $DB->get_record('forum_posts', array('id'=>$postid))) {
  2528. return false;
  2529. }
  2530. if (!$discussion = $DB->get_record('forum_discussions', array('id'=>$post->discussion))) {
  2531. return false;
  2532. }
  2533. if (!$forum = $DB->get_record('forum', array('id'=>$cm->instance))) {
  2534. return false;
  2535. }
  2536. $fs = get_file_storage();
  2537. $relativepath = implode('/', $args);
  2538. $fullpath = "/$context->id/mod_forum/$filearea/$postid/$relativepath";
  2539. if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
  2540. return false;
  2541. }
  2542. // Make sure groups allow this user to see this file
  2543. if ($discussion->groupid > 0) {
  2544. $groupmode = groups_get_activity_groupmode($cm, $course);
  2545. if ($groupmode == SEPARATEGROUPS) {
  2546. if (!groups_is_member($discussion->groupid) and !has_capability('moodle/site:accessallgroups', $context)) {
  2547. return false;
  2548. }
  2549. }
  2550. }
  2551. // Make sure we're allowed to see it...
  2552. if (!forum_user_can_see_post($forum, $discussion, $post, NULL, $cm)) {
  2553. return false;
  2554. }
  2555. // finally send the file
  2556. send_stored_file($file, 0, 0, true, $options); // download MUST be forced - security!
  2557. }
  2558. /**
  2559. * If successful, this function returns the name of the file
  2560. *
  2561. * @global object
  2562. * @param object $post is a full post record, including course and forum
  2563. * @param object $forum
  2564. * @param object $cm
  2565. * @param mixed $mform
  2566. * @param string $unused
  2567. * @return bool
  2568. */
  2569. function forum_add_attachment($post, $forum, $cm, $mform=null, $unused=null) {
  2570. global $DB;
  2571. if (empty($mform)) {
  2572. return false;
  2573. }
  2574. if (empty($post->attachments)) {
  2575. return true; // Nothing to do
  2576. }
  2577. $context = context_module::instance($cm->id);
  2578. $info = file_get_draft_area_info($post->attachments);
  2579. $present = ($info['filecount']>0) ? '1' : '';
  2580. file_save_draft_area_files($post->attachments, $context->id, 'mod_forum', 'attachment', $post->id,
  2581. mod_forum_post_form::attachment_options($forum));
  2582. $DB->set_field('forum_posts', 'attachment', $present, array('id'=>$post->id));
  2583. return true;
  2584. }
  2585. /**
  2586. * Add a new post in an existing discussion.
  2587. *
  2588. * @param stdClass $post The post data
  2589. * @param mixed $mform The submitted form
  2590. * @param string $unused
  2591. * @return int
  2592. */
  2593. function forum_add_new_post($post, $mform, $unused = null) {
  2594. global $USER, $DB;
  2595. $discussion = $DB->get_record('forum_discussions', array('id' => $post->discussion));
  2596. $forum = $DB->get_record('forum', array('id' => $discussion->forum));
  2597. $cm = get_coursemodule_from_instance('forum', $forum->id);
  2598. $context = context_module::instance($cm->id);
  2599. $privatereplyto = 0;
  2600. // Check whether private replies should be enabled for this post.
  2601. if ($post->parent) {
  2602. $parent = $DB->get_record('forum_posts', array('id' => $post->parent));
  2603. if (!empty($parent->privatereplyto)) {
  2604. throw new \coding_exception('It should not be possible to reply to a private reply');
  2605. }
  2606. if (!empty($post->isprivatereply) && forum_user_can_reply_privately($context, $parent)) {
  2607. $privatereplyto = $parent->userid;
  2608. }
  2609. }
  2610. $post->created = $post->modified = time();
  2611. $post->mailed = FORUM_MAILED_PENDING;
  2612. $post->userid = $USER->id;
  2613. $post->privatereplyto = $privatereplyto;
  2614. $post->attachment = "";
  2615. if (!isset($post->totalscore)) {
  2616. $post->totalscore = 0;
  2617. }
  2618. if (!isset($post->mailnow)) {
  2619. $post->mailnow = 0;
  2620. }
  2621. \mod_forum\local\entities\post::add_message_counts($post);
  2622. $post->id = $DB->insert_record("forum_posts", $post);
  2623. $post->message = file_save_draft_area_files($post->itemid, $context->id, 'mod_forum', 'post', $post->id,
  2624. mod_forum_post_form::editor_options($context, null), $post->message);
  2625. $DB->set_field('forum_posts', 'message', $post->message, array('id'=>$post->id));
  2626. forum_add_attachment($post, $forum, $cm, $mform);
  2627. // Update discussion modified date
  2628. $DB->set_field("forum_discussions", "timemodified", $post->modified, array("id" => $post->discussion));
  2629. $DB->set_field("forum_discussions", "usermodified", $post->userid, array("id" => $post->discussion));
  2630. if (forum_tp_can_track_forums($forum) && forum_tp_is_tracked($forum)) {
  2631. forum_tp_mark_post_read($post->userid, $post);
  2632. }
  2633. if (isset($post->tags)) {
  2634. core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, $context, $post->tags);
  2635. }
  2636. // Let Moodle know that assessable content is uploaded (eg for plagiarism detection)
  2637. forum_trigger_content_uploaded_event($post, $cm, 'forum_add_new_post');
  2638. return $post->id;
  2639. }
  2640. /**
  2641. * Trigger post updated event.
  2642. *
  2643. * @param object $post forum post object
  2644. * @param object $discussion discussion object
  2645. * @param object $context forum context object
  2646. * @param object $forum forum object
  2647. * @since Moodle 3.8
  2648. * @return void
  2649. */
  2650. function forum_trigger_post_updated_event($post, $discussion, $context, $forum) {
  2651. global $USER;
  2652. $params = array(
  2653. 'context' => $context,
  2654. 'objectid' => $post->id,
  2655. 'other' => array(
  2656. 'discussionid' => $discussion->id,
  2657. 'forumid' => $forum->id,
  2658. 'forumtype' => $forum->type,
  2659. )
  2660. );
  2661. if ($USER->id !== $post->userid) {
  2662. $params['relateduserid'] = $post->userid;
  2663. }
  2664. $event = \mod_forum\event\post_updated::create($params);
  2665. $event->add_record_snapshot('forum_discussions', $discussion);
  2666. $event->trigger();
  2667. }
  2668. /**
  2669. * Update a post.
  2670. *
  2671. * @param stdClass $newpost The post to update
  2672. * @param mixed $mform The submitted form
  2673. * @param string $unused
  2674. * @return bool
  2675. */
  2676. function forum_update_post($newpost, $mform, $unused = null) {
  2677. global $DB, $USER;
  2678. $post = $DB->get_record('forum_posts', array('id' => $newpost->id));
  2679. $discussion = $DB->get_record('forum_discussions', array('id' => $post->discussion));
  2680. $forum = $DB->get_record('forum', array('id' => $discussion->forum));
  2681. $cm = get_coursemodule_from_instance('forum', $forum->id);
  2682. $context = context_module::instance($cm->id);
  2683. // Allowed modifiable fields.
  2684. $modifiablefields = [
  2685. 'subject',
  2686. 'message',
  2687. 'messageformat',
  2688. 'messagetrust',
  2689. 'timestart',
  2690. 'timeend',
  2691. 'pinned',
  2692. 'attachments',
  2693. ];
  2694. foreach ($modifiablefields as $field) {
  2695. if (isset($newpost->{$field})) {
  2696. $post->{$field} = $newpost->{$field};
  2697. }
  2698. }
  2699. $post->modified = time();
  2700. if (!$post->parent) { // Post is a discussion starter - update discussion title and times too
  2701. $discussion->name = $post->subject;
  2702. $discussion->timestart = $post->timestart;
  2703. $discussion->timeend = $post->timeend;
  2704. if (isset($post->pinned)) {
  2705. $discussion->pinned = $post->pinned;
  2706. }
  2707. }
  2708. $post->message = file_save_draft_area_files($newpost->itemid, $context->id, 'mod_forum', 'post', $post->id,
  2709. mod_forum_post_form::editor_options($context, $post->id), $post->message);
  2710. \mod_forum\local\entities\post::add_message_counts($post);
  2711. $DB->update_record('forum_posts', $post);
  2712. // Note: Discussion modified time/user are intentionally not updated, to enable them to track the latest new post.
  2713. $DB->update_record('forum_discussions', $discussion);
  2714. forum_add_attachment($post, $forum, $cm, $mform);
  2715. if ($forum->type == 'single' && $post->parent == '0') {
  2716. // Updating first post of single discussion type -> updating forum intro.
  2717. $forum->intro = $post->message;
  2718. $forum->timemodified = time();
  2719. $DB->update_record("forum", $forum);
  2720. }
  2721. if (isset($newpost->tags)) {
  2722. core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, $context, $newpost->tags);
  2723. }
  2724. if (forum_tp_can_track_forums($forum) && forum_tp_is_tracked($forum)) {
  2725. forum_tp_mark_post_read($USER->id, $post);
  2726. }
  2727. // Let Moodle know that assessable content is uploaded (eg for plagiarism detection)
  2728. forum_trigger_content_uploaded_event($post, $cm, 'forum_update_post');
  2729. return true;
  2730. }
  2731. /**
  2732. * Given an object containing all the necessary data,
  2733. * create a new discussion and return the id
  2734. *
  2735. * @param object $post
  2736. * @param mixed $mform
  2737. * @param string $unused
  2738. * @param int $userid
  2739. * @return object
  2740. */
  2741. function forum_add_discussion($discussion, $mform=null, $unused=null, $userid=null) {
  2742. global $USER, $CFG, $DB;
  2743. $timenow = isset($discussion->timenow) ? $discussion->timenow : time();
  2744. if (is_null($userid)) {
  2745. $userid = $USER->id;
  2746. }
  2747. // The first post is stored as a real post, and linked
  2748. // to from the discuss entry.
  2749. $forum = $DB->get_record('forum', array('id'=>$discussion->forum));
  2750. $cm = get_coursemodule_from_instance('forum', $forum->id);
  2751. $post = new stdClass();
  2752. $post->discussion = 0;
  2753. $post->parent = 0;
  2754. $post->privatereplyto = 0;
  2755. $post->userid = $userid;
  2756. $post->created = $timenow;
  2757. $post->modified = $timenow;
  2758. $post->mailed = FORUM_MAILED_PENDING;
  2759. $post->subject = $discussion->name;
  2760. $post->message = $discussion->message;
  2761. $post->messageformat = $discussion->messageformat;
  2762. $post->messagetrust = $discussion->messagetrust;
  2763. $post->attachments = isset($discussion->attachments) ? $discussion->attachments : null;
  2764. $post->forum = $forum->id; // speedup
  2765. $post->course = $forum->course; // speedup
  2766. $post->mailnow = $discussion->mailnow;
  2767. \mod_forum\local\entities\post::add_message_counts($post);
  2768. $post->id = $DB->insert_record("forum_posts", $post);
  2769. // TODO: Fix the calling code so that there always is a $cm when this function is called
  2770. if (!empty($cm->id) && !empty($discussion->itemid)) { // In "single simple discussions" this may not exist yet
  2771. $context = context_module::instance($cm->id);
  2772. $text = file_save_draft_area_files($discussion->itemid, $context->id, 'mod_forum', 'post', $post->id,
  2773. mod_forum_post_form::editor_options($context, null), $post->message);
  2774. $DB->set_field('forum_posts', 'message', $text, array('id'=>$post->id));
  2775. }
  2776. // Now do the main entry for the discussion, linking to this first post
  2777. $discussion->firstpost = $post->id;
  2778. $discussion->timemodified = $timenow;
  2779. $discussion->usermodified = $post->userid;
  2780. $discussion->userid = $userid;
  2781. $discussion->assessed = 0;
  2782. $post->discussion = $DB->insert_record("forum_discussions", $discussion);
  2783. // Finally, set the pointer on the post.
  2784. $DB->set_field("forum_posts", "discussion", $post->discussion, array("id"=>$post->id));
  2785. if (!empty($cm->id)) {
  2786. forum_add_attachment($post, $forum, $cm, $mform, $unused);
  2787. }
  2788. if (isset($discussion->tags)) {
  2789. core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, context_module::instance($cm->id), $discussion->tags);
  2790. }
  2791. if (forum_tp_can_track_forums($forum) && forum_tp_is_tracked($forum)) {
  2792. forum_tp_mark_post_read($post->userid, $post);
  2793. }
  2794. // Let Moodle know that assessable content is uploaded (eg for plagiarism detection)
  2795. if (!empty($cm->id)) {
  2796. forum_trigger_content_uploaded_event($post, $cm, 'forum_add_discussion');
  2797. }
  2798. return $post->discussion;
  2799. }
  2800. /**
  2801. * Deletes a discussion and handles all associated cleanup.
  2802. *
  2803. * @global object
  2804. * @param object $discussion Discussion to delete
  2805. * @param bool $fulldelete True when deleting entire forum
  2806. * @param object $course Course
  2807. * @param object $cm Course-module
  2808. * @param object $forum Forum
  2809. * @return bool
  2810. */
  2811. function forum_delete_discussion($discussion, $fulldelete, $course, $cm, $forum) {
  2812. global $DB, $CFG;
  2813. require_once($CFG->libdir.'/completionlib.php');
  2814. $result = true;
  2815. if ($posts = $DB->get_records("forum_posts", array("discussion" => $discussion->id))) {
  2816. foreach ($posts as $post) {
  2817. $post->course = $discussion->course;
  2818. $post->forum = $discussion->forum;
  2819. if (!forum_delete_post($post, 'ignore', $course, $cm, $forum, $fulldelete)) {
  2820. $result = false;
  2821. }
  2822. }
  2823. }
  2824. forum_tp_delete_read_records(-1, -1, $discussion->id);
  2825. // Discussion subscriptions must be removed before discussions because of key constraints.
  2826. $DB->delete_records('forum_discussion_subs', array('discussion' => $discussion->id));
  2827. if (!$DB->delete_records("forum_discussions", array("id" => $discussion->id))) {
  2828. $result = false;
  2829. }
  2830. // Update completion state if we are tracking completion based on number of posts
  2831. // But don't bother when deleting whole thing
  2832. if (!$fulldelete) {
  2833. $completion = new completion_info($course);
  2834. if ($completion->is_enabled($cm) == COMPLETION_TRACKING_AUTOMATIC &&
  2835. ($forum->completiondiscussions || $forum->completionreplies || $forum->completionposts)) {
  2836. $completion->update_state($cm, COMPLETION_INCOMPLETE, $discussion->userid);
  2837. }
  2838. }
  2839. $params = array(
  2840. 'objectid' => $discussion->id,
  2841. 'context' => context_module::instance($cm->id),
  2842. 'other' => array(
  2843. 'forumid' => $forum->id,
  2844. )
  2845. );
  2846. $event = \mod_forum\event\discussion_deleted::create($params);
  2847. $event->add_record_snapshot('forum_discussions', $discussion);
  2848. $event->trigger();
  2849. return $result;
  2850. }
  2851. /**
  2852. * Deletes a single forum post.
  2853. *
  2854. * @global object
  2855. * @param object $post Forum post object
  2856. * @param mixed $children Whether to delete children. If false, returns false
  2857. * if there are any children (without deleting the post). If true,
  2858. * recursively deletes all children. If set to special value 'ignore', deletes
  2859. * post regardless of children (this is for use only when deleting all posts
  2860. * in a disussion).
  2861. * @param object $course Course
  2862. * @param object $cm Course-module
  2863. * @param object $forum Forum
  2864. * @param bool $skipcompletion True to skip updating completion state if it
  2865. * would otherwise be updated, i.e. when deleting entire forum anyway.
  2866. * @return bool
  2867. */
  2868. function forum_delete_post($post, $children, $course, $cm, $forum, $skipcompletion=false) {
  2869. global $DB, $CFG, $USER;
  2870. require_once($CFG->libdir.'/completionlib.php');
  2871. $context = context_module::instance($cm->id);
  2872. if ($children !== 'ignore' && ($childposts = $DB->get_records('forum_posts', array('parent'=>$post->id)))) {
  2873. if ($children) {
  2874. foreach ($childposts as $childpost) {
  2875. forum_delete_post($childpost, true, $course, $cm, $forum, $skipcompletion);
  2876. }
  2877. } else {
  2878. return false;
  2879. }
  2880. }
  2881. // Delete ratings.
  2882. require_once($CFG->dirroot.'/rating/lib.php');
  2883. $delopt = new stdClass;
  2884. $delopt->contextid = $context->id;
  2885. $delopt->component = 'mod_forum';
  2886. $delopt->ratingarea = 'post';
  2887. $delopt->itemid = $post->id;
  2888. $rm = new rating_manager();
  2889. $rm->delete_ratings($delopt);
  2890. // Delete attachments.
  2891. $fs = get_file_storage();
  2892. $fs->delete_area_files($context->id, 'mod_forum', 'attachment', $post->id);
  2893. $fs->delete_area_files($context->id, 'mod_forum', 'post', $post->id);
  2894. // Delete cached RSS feeds.
  2895. if (!empty($CFG->enablerssfeeds)) {
  2896. require_once($CFG->dirroot.'/mod/forum/rsslib.php');
  2897. forum_rss_delete_file($forum);
  2898. }
  2899. if ($DB->delete_records("forum_posts", array("id" => $post->id))) {
  2900. forum_tp_delete_read_records(-1, $post->id);
  2901. // Just in case we are deleting the last post
  2902. forum_discussion_update_last_post($post->discussion);
  2903. // Update completion state if we are tracking completion based on number of posts
  2904. // But don't bother when deleting whole thing
  2905. if (!$skipcompletion) {
  2906. $completion = new completion_info($course);
  2907. if ($completion->is_enabled($cm) == COMPLETION_TRACKING_AUTOMATIC &&
  2908. ($forum->completiondiscussions || $forum->completionreplies || $forum->completionposts)) {
  2909. $completion->update_state($cm, COMPLETION_INCOMPLETE, $post->userid);
  2910. }
  2911. }
  2912. $params = array(
  2913. 'context' => $context,
  2914. 'objectid' => $post->id,
  2915. 'other' => array(
  2916. 'discussionid' => $post->discussion,
  2917. 'forumid' => $forum->id,
  2918. 'forumtype' => $forum->type,
  2919. )
  2920. );
  2921. $post->deleted = 1;
  2922. if ($post->userid !== $USER->id) {
  2923. $params['relateduserid'] = $post->userid;
  2924. }
  2925. $event = \mod_forum\event\post_deleted::create($params);
  2926. $event->add_record_snapshot('forum_posts', $post);
  2927. $event->trigger();
  2928. return true;
  2929. }
  2930. return false;
  2931. }
  2932. /**
  2933. * Sends post content to plagiarism plugin
  2934. * @param object $post Forum post object
  2935. * @param object $cm Course-module
  2936. * @param string $name
  2937. * @return bool
  2938. */
  2939. function forum_trigger_content_uploaded_event($post, $cm, $name) {
  2940. $context = context_module::instance($cm->id);
  2941. $fs = get_file_storage();
  2942. $files = $fs->get_area_files($context->id, 'mod_forum', 'attachment', $post->id, "timemodified", false);
  2943. $params = array(
  2944. 'context' => $context,
  2945. 'objectid' => $post->id,
  2946. 'other' => array(
  2947. 'content' => $post->message,
  2948. 'pathnamehashes' => array_keys($files),
  2949. 'discussionid' => $post->discussion,
  2950. 'triggeredfrom' => $name,
  2951. )
  2952. );
  2953. $event = \mod_forum\event\assessable_uploaded::create($params);
  2954. $event->trigger();
  2955. return true;
  2956. }
  2957. /**
  2958. * Given a new post, subscribes or unsubscribes as appropriate.
  2959. * Returns some text which describes what happened.
  2960. *
  2961. * @param object $fromform The submitted form
  2962. * @param stdClass $forum The forum record
  2963. * @param stdClass $discussion The forum discussion record
  2964. * @return string
  2965. */
  2966. function forum_post_subscription($fromform, $forum, $discussion) {
  2967. global $USER;
  2968. if (\mod_forum\subscriptions::is_forcesubscribed($forum)) {
  2969. return "";
  2970. } else if (\mod_forum\subscriptions::subscription_disabled($forum)) {
  2971. $subscribed = \mod_forum\subscriptions::is_subscribed($USER->id, $forum);
  2972. if ($subscribed && !has_capability('moodle/course:manageactivities', context_course::instance($forum->course), $USER->id)) {
  2973. // This user should not be subscribed to the forum.
  2974. \mod_forum\subscriptions::unsubscribe_user($USER->id, $forum);
  2975. }
  2976. return "";
  2977. }
  2978. $info = new stdClass();
  2979. $info->name = fullname($USER);
  2980. $info->discussion = format_string($discussion->name);
  2981. $info->forum = format_string($forum->name);
  2982. if (isset($fromform->discussionsubscribe) && $fromform->discussionsubscribe) {
  2983. if ($result = \mod_forum\subscriptions::subscribe_user_to_discussion($USER->id, $discussion)) {
  2984. return html_writer::tag('p', get_string('discussionnowsubscribed', 'forum', $info));
  2985. }
  2986. } else {
  2987. if ($result = \mod_forum\subscriptions::unsubscribe_user_from_discussion($USER->id, $discussion)) {
  2988. return html_writer::tag('p', get_string('discussionnownotsubscribed', 'forum', $info));
  2989. }
  2990. }
  2991. return '';
  2992. }
  2993. /**
  2994. * Generate and return the subscribe or unsubscribe link for a forum.
  2995. *
  2996. * @param object $forum the forum. Fields used are $forum->id and $forum->forcesubscribe.
  2997. * @param object $context the context object for this forum.
  2998. * @param array $messages text used for the link in its various states
  2999. * (subscribed, unsubscribed, forcesubscribed or cantsubscribe).
  3000. * Any strings not passed in are taken from the $defaultmessages array
  3001. * at the top of the function.
  3002. * @param bool $cantaccessagroup
  3003. * @param bool $unused1
  3004. * @param bool $backtoindex
  3005. * @param array $unused2
  3006. * @return string
  3007. */
  3008. function forum_get_subscribe_link($forum, $context, $messages = array(), $cantaccessagroup = false, $unused1 = true,
  3009. $backtoindex = false, $unused2 = null) {
  3010. global $CFG, $USER, $PAGE, $OUTPUT;
  3011. $defaultmessages = array(
  3012. 'subscribed' => get_string('unsubscribe', 'forum'),
  3013. 'unsubscribed' => get_string('subscribe', 'forum'),
  3014. 'cantaccessgroup' => get_string('no'),
  3015. 'forcesubscribed' => get_string('everyoneissubscribed', 'forum'),
  3016. 'cantsubscribe' => get_string('disallowsubscribe','forum')
  3017. );
  3018. $messages = $messages + $defaultmessages;
  3019. if (\mod_forum\subscriptions::is_forcesubscribed($forum)) {
  3020. return $messages['forcesubscribed'];
  3021. } else if (\mod_forum\subscriptions::subscription_disabled($forum) &&
  3022. !has_capability('mod/forum:managesubscriptions', $context)) {
  3023. return $messages['cantsubscribe'];
  3024. } else if ($cantaccessagroup) {
  3025. return $messages['cantaccessgroup'];
  3026. } else {
  3027. if (!is_enrolled($context, $USER, '', true)) {
  3028. return '';
  3029. }
  3030. $subscribed = \mod_forum\subscriptions::is_subscribed($USER->id, $forum);
  3031. if ($subscribed) {
  3032. $linktext = $messages['subscribed'];
  3033. $linktitle = get_string('subscribestop', 'forum');
  3034. } else {
  3035. $linktext = $messages['unsubscribed'];
  3036. $linktitle = get_string('subscribestart', 'forum');
  3037. }
  3038. $options = array();
  3039. if ($backtoindex) {
  3040. $backtoindexlink = '&amp;backtoindex=1';
  3041. $options['backtoindex'] = 1;
  3042. } else {
  3043. $backtoindexlink = '';
  3044. }
  3045. $options['id'] = $forum->id;
  3046. $options['sesskey'] = sesskey();
  3047. $url = new moodle_url('/mod/forum/subscribe.php', $options);
  3048. return $OUTPUT->single_button($url, $linktext, 'get', array('title' => $linktitle));
  3049. }
  3050. }
  3051. /**
  3052. * Returns true if user created new discussion already.
  3053. *
  3054. * @param int $forumid The forum to check for postings
  3055. * @param int $userid The user to check for postings
  3056. * @param int $groupid The group to restrict the check to
  3057. * @return bool
  3058. */
  3059. function forum_user_has_posted_discussion($forumid, $userid, $groupid = null) {
  3060. global $CFG, $DB;
  3061. $sql = "SELECT 'x'
  3062. FROM {forum_discussions} d, {forum_posts} p
  3063. WHERE d.forum = ? AND p.discussion = d.id AND p.parent = 0 AND p.userid = ?";
  3064. $params = [$forumid, $userid];
  3065. if ($groupid) {
  3066. $sql .= " AND d.groupid = ?";
  3067. $params[] = $groupid;
  3068. }
  3069. return $DB->record_exists_sql($sql, $params);
  3070. }
  3071. /**
  3072. * @global object
  3073. * @global object
  3074. * @param int $forumid
  3075. * @param int $userid
  3076. * @return array
  3077. */
  3078. function forum_discussions_user_has_posted_in($forumid, $userid) {
  3079. global $CFG, $DB;
  3080. $haspostedsql = "SELECT d.id AS id,
  3081. d.*
  3082. FROM {forum_posts} p,
  3083. {forum_discussions} d
  3084. WHERE p.discussion = d.id
  3085. AND d.forum = ?
  3086. AND p.userid = ?";
  3087. return $DB->get_records_sql($haspostedsql, array($forumid, $userid));
  3088. }
  3089. /**
  3090. * @global object
  3091. * @global object
  3092. * @param int $forumid
  3093. * @param int $did
  3094. * @param int $userid
  3095. * @return bool
  3096. */
  3097. function forum_user_has_posted($forumid, $did, $userid) {
  3098. global $DB;
  3099. if (empty($did)) {
  3100. // posted in any forum discussion?
  3101. $sql = "SELECT 'x'
  3102. FROM {forum_posts} p
  3103. JOIN {forum_discussions} d ON d.id = p.discussion
  3104. WHERE p.userid = :userid AND d.forum = :forumid";
  3105. return $DB->record_exists_sql($sql, array('forumid'=>$forumid,'userid'=>$userid));
  3106. } else {
  3107. return $DB->record_exists('forum_posts', array('discussion'=>$did,'userid'=>$userid));
  3108. }
  3109. }
  3110. /**
  3111. * Returns creation time of the first user's post in given discussion
  3112. * @global object $DB
  3113. * @param int $did Discussion id
  3114. * @param int $userid User id
  3115. * @return int|bool post creation time stamp or return false
  3116. */
  3117. function forum_get_user_posted_time($did, $userid) {
  3118. global $DB;
  3119. $posttime = $DB->get_field('forum_posts', 'MIN(created)', array('userid'=>$userid, 'discussion'=>$did));
  3120. if (empty($posttime)) {
  3121. return false;
  3122. }
  3123. return $posttime;
  3124. }
  3125. /**
  3126. * @global object
  3127. * @param object $forum
  3128. * @param object $currentgroup
  3129. * @param int $unused
  3130. * @param object $cm
  3131. * @param object $context
  3132. * @return bool
  3133. */
  3134. function forum_user_can_post_discussion($forum, $currentgroup=null, $unused=-1, $cm=NULL, $context=NULL) {
  3135. // $forum is an object
  3136. global $USER;
  3137. // shortcut - guest and not-logged-in users can not post
  3138. if (isguestuser() or !isloggedin()) {
  3139. return false;
  3140. }
  3141. if (!$cm) {
  3142. debugging('missing cm', DEBUG_DEVELOPER);
  3143. if (!$cm = get_coursemodule_from_instance('forum', $forum->id, $forum->course)) {
  3144. print_error('invalidcoursemodule');
  3145. }
  3146. }
  3147. if (!$context) {
  3148. $context = context_module::instance($cm->id);
  3149. }
  3150. if (forum_is_cutoff_date_reached($forum)) {
  3151. if (!has_capability('mod/forum:canoverridecutoff', $context)) {
  3152. return false;
  3153. }
  3154. }
  3155. if ($currentgroup === null) {
  3156. $currentgroup = groups_get_activity_group($cm);
  3157. }
  3158. $groupmode = groups_get_activity_groupmode($cm);
  3159. if ($forum->type == 'news') {
  3160. $capname = 'mod/forum:addnews';
  3161. } else if ($forum->type == 'qanda') {
  3162. $capname = 'mod/forum:addquestion';
  3163. } else {
  3164. $capname = 'mod/forum:startdiscussion';
  3165. }
  3166. if (!has_capability($capname, $context)) {
  3167. return false;
  3168. }
  3169. if ($forum->type == 'single') {
  3170. return false;
  3171. }
  3172. if ($forum->type == 'eachuser') {
  3173. if (forum_user_has_posted_discussion($forum->id, $USER->id, $currentgroup)) {
  3174. return false;
  3175. }
  3176. }
  3177. if (!$groupmode or has_capability('moodle/site:accessallgroups', $context)) {
  3178. return true;
  3179. }
  3180. if ($currentgroup) {
  3181. return groups_is_member($currentgroup);
  3182. } else {
  3183. // no group membership and no accessallgroups means no new discussions
  3184. // reverted to 1.7 behaviour in 1.9+, buggy in 1.8.0-1.9.0
  3185. return false;
  3186. }
  3187. }
  3188. /**
  3189. * This function checks whether the user can reply to posts in a forum
  3190. * discussion. Use forum_user_can_post_discussion() to check whether the user
  3191. * can start discussions.
  3192. *
  3193. * @global object
  3194. * @global object
  3195. * @uses DEBUG_DEVELOPER
  3196. * @uses CONTEXT_MODULE
  3197. * @uses VISIBLEGROUPS
  3198. * @param object $forum forum object
  3199. * @param object $discussion
  3200. * @param object $user
  3201. * @param object $cm
  3202. * @param object $course
  3203. * @param object $context
  3204. * @return bool
  3205. */
  3206. function forum_user_can_post($forum, $discussion, $user=NULL, $cm=NULL, $course=NULL, $context=NULL) {
  3207. global $USER, $DB;
  3208. if (empty($user)) {
  3209. $user = $USER;
  3210. }
  3211. // shortcut - guest and not-logged-in users can not post
  3212. if (isguestuser($user) or empty($user->id)) {
  3213. return false;
  3214. }
  3215. if (!isset($discussion->groupid)) {
  3216. debugging('incorrect discussion parameter', DEBUG_DEVELOPER);
  3217. return false;
  3218. }
  3219. if (!$cm) {
  3220. debugging('missing cm', DEBUG_DEVELOPER);
  3221. if (!$cm = get_coursemodule_from_instance('forum', $forum->id, $forum->course)) {
  3222. print_error('invalidcoursemodule');
  3223. }
  3224. }
  3225. if (!$course) {
  3226. debugging('missing course', DEBUG_DEVELOPER);
  3227. if (!$course = $DB->get_record('course', array('id' => $forum->course))) {
  3228. print_error('invalidcourseid');
  3229. }
  3230. }
  3231. if (!$context) {
  3232. $context = context_module::instance($cm->id);
  3233. }
  3234. if (forum_is_cutoff_date_reached($forum)) {
  3235. if (!has_capability('mod/forum:canoverridecutoff', $context)) {
  3236. return false;
  3237. }
  3238. }
  3239. // Check whether the discussion is locked.
  3240. if (forum_discussion_is_locked($forum, $discussion)) {
  3241. if (!has_capability('mod/forum:canoverridediscussionlock', $context)) {
  3242. return false;
  3243. }
  3244. }
  3245. // normal users with temporary guest access can not post, suspended users can not post either
  3246. if (!is_viewing($context, $user->id) and !is_enrolled($context, $user->id, '', true)) {
  3247. return false;
  3248. }
  3249. if ($forum->type == 'news') {
  3250. $capname = 'mod/forum:replynews';
  3251. } else {
  3252. $capname = 'mod/forum:replypost';
  3253. }
  3254. if (!has_capability($capname, $context, $user->id)) {
  3255. return false;
  3256. }
  3257. if (!$groupmode = groups_get_activity_groupmode($cm, $course)) {
  3258. return true;
  3259. }
  3260. if (has_capability('moodle/site:accessallgroups', $context)) {
  3261. return true;
  3262. }
  3263. if ($groupmode == VISIBLEGROUPS) {
  3264. if ($discussion->groupid == -1) {
  3265. // allow students to reply to all participants discussions - this was not possible in Moodle <1.8
  3266. return true;
  3267. }
  3268. return groups_is_member($discussion->groupid);
  3269. } else {
  3270. //separate groups
  3271. if ($discussion->groupid == -1) {
  3272. return false;
  3273. }
  3274. return groups_is_member($discussion->groupid);
  3275. }
  3276. }
  3277. /**
  3278. * Check to ensure a user can view a timed discussion.
  3279. *
  3280. * @param object $discussion
  3281. * @param object $user
  3282. * @param object $context
  3283. * @return boolean returns true if they can view post, false otherwise
  3284. */
  3285. function forum_user_can_see_timed_discussion($discussion, $user, $context) {
  3286. global $CFG;
  3287. // Check that the user can view a discussion that is normally hidden due to access times.
  3288. if (!empty($CFG->forum_enabletimedposts)) {
  3289. $time = time();
  3290. if (($discussion->timestart != 0 && $discussion->timestart > $time)
  3291. || ($discussion->timeend != 0 && $discussion->timeend < $time)) {
  3292. if (!has_capability('mod/forum:viewhiddentimedposts', $context, $user->id)) {
  3293. return false;
  3294. }
  3295. }
  3296. }
  3297. return true;
  3298. }
  3299. /**
  3300. * Check to ensure a user can view a group discussion.
  3301. *
  3302. * @param object $discussion
  3303. * @param object $cm
  3304. * @param object $context
  3305. * @return boolean returns true if they can view post, false otherwise
  3306. */
  3307. function forum_user_can_see_group_discussion($discussion, $cm, $context) {
  3308. // If it's a grouped discussion, make sure the user is a member.
  3309. if ($discussion->groupid > 0) {
  3310. $groupmode = groups_get_activity_groupmode($cm);
  3311. if ($groupmode == SEPARATEGROUPS) {
  3312. return groups_is_member($discussion->groupid) || has_capability('moodle/site:accessallgroups', $context);
  3313. }
  3314. }
  3315. return true;
  3316. }
  3317. /**
  3318. * @global object
  3319. * @global object
  3320. * @uses DEBUG_DEVELOPER
  3321. * @param object $forum
  3322. * @param object $discussion
  3323. * @param object $context
  3324. * @param object $user
  3325. * @return bool
  3326. */
  3327. function forum_user_can_see_discussion($forum, $discussion, $context, $user=NULL) {
  3328. global $USER, $DB;
  3329. if (empty($user) || empty($user->id)) {
  3330. $user = $USER;
  3331. }
  3332. // retrieve objects (yuk)
  3333. if (is_numeric($forum)) {
  3334. debugging('missing full forum', DEBUG_DEVELOPER);
  3335. if (!$forum = $DB->get_record('forum',array('id'=>$forum))) {
  3336. return false;
  3337. }
  3338. }
  3339. if (is_numeric($discussion)) {
  3340. debugging('missing full discussion', DEBUG_DEVELOPER);
  3341. if (!$discussion = $DB->get_record('forum_discussions',array('id'=>$discussion))) {
  3342. return false;
  3343. }
  3344. }
  3345. if (!$cm = get_coursemodule_from_instance('forum', $forum->id, $forum->course)) {
  3346. print_error('invalidcoursemodule');
  3347. }
  3348. if (!has_capability('mod/forum:viewdiscussion', $context)) {
  3349. return false;
  3350. }
  3351. if (!forum_user_can_see_timed_discussion($discussion, $user, $context)) {
  3352. return false;
  3353. }
  3354. if (!forum_user_can_see_group_discussion($discussion, $cm, $context)) {
  3355. return false;
  3356. }
  3357. return true;
  3358. }
  3359. /**
  3360. * Check whether a user can see the specified post.
  3361. *
  3362. * @param \stdClass $forum The forum to chcek
  3363. * @param \stdClass $discussion The discussion the post is in
  3364. * @param \stdClass $post The post in question
  3365. * @param \stdClass $user The user to test - if not specified, the current user is checked.
  3366. * @param \stdClass $cm The Course Module that the forum is in (required).
  3367. * @param bool $checkdeleted Whether to check the deleted flag on the post.
  3368. * @return bool
  3369. */
  3370. function forum_user_can_see_post($forum, $discussion, $post, $user = null, $cm = null, $checkdeleted = true) {
  3371. global $CFG, $USER, $DB;
  3372. // retrieve objects (yuk)
  3373. if (is_numeric($forum)) {
  3374. debugging('missing full forum', DEBUG_DEVELOPER);
  3375. if (!$forum = $DB->get_record('forum',array('id'=>$forum))) {
  3376. return false;
  3377. }
  3378. }
  3379. if (is_numeric($discussion)) {
  3380. debugging('missing full discussion', DEBUG_DEVELOPER);
  3381. if (!$discussion = $DB->get_record('forum_discussions',array('id'=>$discussion))) {
  3382. return false;
  3383. }
  3384. }
  3385. if (is_numeric($post)) {
  3386. debugging('missing full post', DEBUG_DEVELOPER);
  3387. if (!$post = $DB->get_record('forum_posts',array('id'=>$post))) {
  3388. return false;
  3389. }
  3390. }
  3391. if (!isset($post->id) && isset($post->parent)) {
  3392. $post->id = $post->parent;
  3393. }
  3394. if ($checkdeleted && !empty($post->deleted)) {
  3395. return false;
  3396. }
  3397. if (!$cm) {
  3398. debugging('missing cm', DEBUG_DEVELOPER);
  3399. if (!$cm = get_coursemodule_from_instance('forum', $forum->id, $forum->course)) {
  3400. print_error('invalidcoursemodule');
  3401. }
  3402. }
  3403. // Context used throughout function.
  3404. $modcontext = context_module::instance($cm->id);
  3405. if (empty($user) || empty($user->id)) {
  3406. $user = $USER;
  3407. }
  3408. $canviewdiscussion = (isset($cm->cache) && !empty($cm->cache->caps['mod/forum:viewdiscussion']))
  3409. || has_capability('mod/forum:viewdiscussion', $modcontext, $user->id);
  3410. if (!$canviewdiscussion && !has_all_capabilities(array('moodle/user:viewdetails', 'moodle/user:readuserposts'), context_user::instance($post->userid))) {
  3411. return false;
  3412. }
  3413. if (!forum_post_is_visible_privately($post, $cm)) {
  3414. return false;
  3415. }
  3416. if (isset($cm->uservisible)) {
  3417. if (!$cm->uservisible) {
  3418. return false;
  3419. }
  3420. } else {
  3421. if (!\core_availability\info_module::is_user_visible($cm, $user->id, false)) {
  3422. return false;
  3423. }
  3424. }
  3425. if (!forum_user_can_see_timed_discussion($discussion, $user, $modcontext)) {
  3426. return false;
  3427. }
  3428. if (!forum_user_can_see_group_discussion($discussion, $cm, $modcontext)) {
  3429. return false;
  3430. }
  3431. if ($forum->type == 'qanda') {
  3432. if (has_capability('mod/forum:viewqandawithoutposting', $modcontext, $user->id) || $post->userid == $user->id
  3433. || (isset($discussion->firstpost) && $discussion->firstpost == $post->id)) {
  3434. return true;
  3435. }
  3436. $firstpost = forum_get_firstpost_from_discussion($discussion->id);
  3437. if ($firstpost->userid == $user->id) {
  3438. return true;
  3439. }
  3440. $userfirstpost = forum_get_user_posted_time($discussion->id, $user->id);
  3441. return (($userfirstpost !== false && (time() - $userfirstpost >= $CFG->maxeditingtime)));
  3442. }
  3443. return true;
  3444. }
  3445. /**
  3446. * Returns all forum posts since a given time in specified forum.
  3447. *
  3448. * @todo Document this functions args
  3449. * @global object
  3450. * @global object
  3451. * @global object
  3452. * @global object
  3453. */
  3454. function forum_get_recent_mod_activity(&$activities, &$index, $timestart, $courseid, $cmid, $userid=0, $groupid=0) {
  3455. global $CFG, $COURSE, $USER, $DB;
  3456. if ($COURSE->id == $courseid) {
  3457. $course = $COURSE;
  3458. } else {
  3459. $course = $DB->get_record('course', array('id' => $courseid));
  3460. }
  3461. $modinfo = get_fast_modinfo($course);
  3462. $cm = $modinfo->cms[$cmid];
  3463. $params = array($timestart, $cm->instance);
  3464. if ($userid) {
  3465. $userselect = "AND u.id = ?";
  3466. $params[] = $userid;
  3467. } else {
  3468. $userselect = "";
  3469. }
  3470. if ($groupid) {
  3471. $groupselect = "AND d.groupid = ?";
  3472. $params[] = $groupid;
  3473. } else {
  3474. $groupselect = "";
  3475. }
  3476. $userfieldsapi = \core_user\fields::for_name();
  3477. $allnames = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
  3478. if (!$posts = $DB->get_records_sql("SELECT p.*, f.type AS forumtype, d.forum, d.groupid,
  3479. d.timestart, d.timeend, d.userid AS duserid,
  3480. $allnames, u.email, u.picture, u.imagealt, u.email
  3481. FROM {forum_posts} p
  3482. JOIN {forum_discussions} d ON d.id = p.discussion
  3483. JOIN {forum} f ON f.id = d.forum
  3484. JOIN {user} u ON u.id = p.userid
  3485. WHERE p.created > ? AND f.id = ?
  3486. $userselect $groupselect
  3487. ORDER BY p.id ASC", $params)) { // order by initial posting date
  3488. return;
  3489. }
  3490. $groupmode = groups_get_activity_groupmode($cm, $course);
  3491. $cm_context = context_module::instance($cm->id);
  3492. $viewhiddentimed = has_capability('mod/forum:viewhiddentimedposts', $cm_context);
  3493. $accessallgroups = has_capability('moodle/site:accessallgroups', $cm_context);
  3494. $printposts = array();
  3495. foreach ($posts as $post) {
  3496. if (!empty($CFG->forum_enabletimedposts) and $USER->id != $post->duserid
  3497. and (($post->timestart > 0 and $post->timestart > time()) or ($post->timeend > 0 and $post->timeend < time()))) {
  3498. if (!$viewhiddentimed) {
  3499. continue;
  3500. }
  3501. }
  3502. if ($groupmode) {
  3503. if ($post->groupid == -1 or $groupmode == VISIBLEGROUPS or $accessallgroups) {
  3504. // oki (Open discussions have groupid -1)
  3505. } else {
  3506. // separate mode
  3507. if (isguestuser()) {
  3508. // shortcut
  3509. continue;
  3510. }
  3511. if (!in_array($post->groupid, $modinfo->get_groups($cm->groupingid))) {
  3512. continue;
  3513. }
  3514. }
  3515. }
  3516. $printposts[] = $post;
  3517. }
  3518. if (!$printposts) {
  3519. return;
  3520. }
  3521. $aname = format_string($cm->name,true);
  3522. foreach ($printposts as $post) {
  3523. $tmpactivity = new stdClass();
  3524. $tmpactivity->type = 'forum';
  3525. $tmpactivity->cmid = $cm->id;
  3526. $tmpactivity->name = $aname;
  3527. $tmpactivity->sectionnum = $cm->sectionnum;
  3528. $tmpactivity->timestamp = $post->modified;
  3529. $tmpactivity->content = new stdClass();
  3530. $tmpactivity->content->id = $post->id;
  3531. $tmpactivity->content->discussion = $post->discussion;
  3532. $tmpactivity->content->subject = format_string($post->subject);
  3533. $tmpactivity->content->parent = $post->parent;
  3534. $tmpactivity->content->forumtype = $post->forumtype;
  3535. $tmpactivity->user = new stdClass();
  3536. $additionalfields = array('id' => 'userid', 'picture', 'imagealt', 'email');
  3537. $additionalfields = explode(',', implode(',', \core_user\fields::get_picture_fields()));
  3538. $tmpactivity->user = username_load_fields_from_object($tmpactivity->user, $post, null, $additionalfields);
  3539. $tmpactivity->user->id = $post->userid;
  3540. $activities[$index++] = $tmpactivity;
  3541. }
  3542. return;
  3543. }
  3544. /**
  3545. * Outputs the forum post indicated by $activity.
  3546. *
  3547. * @param object $activity the activity object the forum resides in
  3548. * @param int $courseid the id of the course the forum resides in
  3549. * @param bool $detail not used, but required for compatibilty with other modules
  3550. * @param int $modnames not used, but required for compatibilty with other modules
  3551. * @param bool $viewfullnames not used, but required for compatibilty with other modules
  3552. */
  3553. function forum_print_recent_mod_activity($activity, $courseid, $detail, $modnames, $viewfullnames) {
  3554. global $OUTPUT;
  3555. $content = $activity->content;
  3556. if ($content->parent) {
  3557. $class = 'reply';
  3558. } else {
  3559. $class = 'discussion';
  3560. }
  3561. $tableoptions = [
  3562. 'border' => '0',
  3563. 'cellpadding' => '3',
  3564. 'cellspacing' => '0',
  3565. 'class' => 'forum-recent'
  3566. ];
  3567. $output = html_writer::start_tag('table', $tableoptions);
  3568. $output .= html_writer::start_tag('tr');
  3569. $post = (object) ['parent' => $content->parent];
  3570. $forum = (object) ['type' => $content->forumtype];
  3571. $authorhidden = forum_is_author_hidden($post, $forum);
  3572. // Show user picture if author should not be hidden.
  3573. if (!$authorhidden) {
  3574. $pictureoptions = [
  3575. 'courseid' => $courseid,
  3576. 'link' => $authorhidden,
  3577. 'alttext' => $authorhidden,
  3578. ];
  3579. $picture = $OUTPUT->user_picture($activity->user, $pictureoptions);
  3580. $output .= html_writer::tag('td', $picture, ['class' => 'userpicture', 'valign' => 'top']);
  3581. }
  3582. // Discussion title and author.
  3583. $output .= html_writer::start_tag('td', ['class' => $class]);
  3584. if ($content->parent) {
  3585. $class = 'title';
  3586. } else {
  3587. // Bold the title of new discussions so they stand out.
  3588. $class = 'title bold';
  3589. }
  3590. $output .= html_writer::start_div($class);
  3591. if ($detail) {
  3592. $aname = s($activity->name);
  3593. $output .= $OUTPUT->image_icon('icon', $aname, $activity->type);
  3594. }
  3595. $discussionurl = new moodle_url('/mod/forum/discuss.php', ['d' => $content->discussion]);
  3596. $discussionurl->set_anchor('p' . $activity->content->id);
  3597. $output .= html_writer::link($discussionurl, $content->subject);
  3598. $output .= html_writer::end_div();
  3599. $timestamp = userdate_htmltime($activity->timestamp);
  3600. if ($authorhidden) {
  3601. $authornamedate = $timestamp;
  3602. } else {
  3603. $fullname = fullname($activity->user, $viewfullnames);
  3604. $userurl = new moodle_url('/user/view.php');
  3605. $userurl->params(['id' => $activity->user->id, 'course' => $courseid]);
  3606. $by = new stdClass();
  3607. $by->name = html_writer::link($userurl, $fullname);
  3608. $by->date = $timestamp;
  3609. $authornamedate = get_string('bynameondate', 'forum', $by);
  3610. }
  3611. $output .= html_writer::div($authornamedate, 'user');
  3612. $output .= html_writer::end_tag('td');
  3613. $output .= html_writer::end_tag('tr');
  3614. $output .= html_writer::end_tag('table');
  3615. echo $output;
  3616. }
  3617. /**
  3618. * recursively sets the discussion field to $discussionid on $postid and all its children
  3619. * used when pruning a post
  3620. *
  3621. * @global object
  3622. * @param int $postid
  3623. * @param int $discussionid
  3624. * @return bool
  3625. */
  3626. function forum_change_discussionid($postid, $discussionid) {
  3627. global $DB;
  3628. $DB->set_field('forum_posts', 'discussion', $discussionid, array('id' => $postid));
  3629. if ($posts = $DB->get_records('forum_posts', array('parent' => $postid))) {
  3630. foreach ($posts as $post) {
  3631. forum_change_discussionid($post->id, $discussionid);
  3632. }
  3633. }
  3634. return true;
  3635. }
  3636. /**
  3637. * Prints the editing button on subscribers page
  3638. *
  3639. * @global object
  3640. * @global object
  3641. * @param int $courseid
  3642. * @param int $forumid
  3643. * @return string
  3644. */
  3645. function forum_update_subscriptions_button($courseid, $forumid) {
  3646. global $CFG, $USER;
  3647. if (!empty($USER->subscriptionsediting)) {
  3648. $string = get_string('managesubscriptionsoff', 'forum');
  3649. $edit = "off";
  3650. } else {
  3651. $string = get_string('managesubscriptionson', 'forum');
  3652. $edit = "on";
  3653. }
  3654. $subscribers = html_writer::start_tag('form', array('action' => $CFG->wwwroot . '/mod/forum/subscribers.php',
  3655. 'method' => 'get', 'class' => 'form-inline'));
  3656. $subscribers .= html_writer::empty_tag('input', array('type' => 'submit', 'value' => $string,
  3657. 'class' => 'btn btn-secondary'));
  3658. $subscribers .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'id', 'value' => $forumid));
  3659. $subscribers .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'edit', 'value' => $edit));
  3660. $subscribers .= html_writer::end_tag('form');
  3661. return $subscribers;
  3662. }
  3663. // Functions to do with read tracking.
  3664. /**
  3665. * Mark posts as read.
  3666. *
  3667. * @global object
  3668. * @global object
  3669. * @param object $user object
  3670. * @param array $postids array of post ids
  3671. * @return boolean success
  3672. */
  3673. function forum_tp_mark_posts_read($user, $postids) {
  3674. global $CFG, $DB;
  3675. if (!forum_tp_can_track_forums(false, $user)) {
  3676. return true;
  3677. }
  3678. $status = true;
  3679. $now = time();
  3680. $cutoffdate = $now - ($CFG->forum_oldpostdays * 24 * 3600);
  3681. if (empty($postids)) {
  3682. return true;
  3683. } else if (count($postids) > 200) {
  3684. while ($part = array_splice($postids, 0, 200)) {
  3685. $status = forum_tp_mark_posts_read($user, $part) && $status;
  3686. }
  3687. return $status;
  3688. }
  3689. list($usql, $postidparams) = $DB->get_in_or_equal($postids, SQL_PARAMS_NAMED, 'postid');
  3690. $insertparams = array(
  3691. 'userid1' => $user->id,
  3692. 'userid2' => $user->id,
  3693. 'userid3' => $user->id,
  3694. 'firstread' => $now,
  3695. 'lastread' => $now,
  3696. 'cutoffdate' => $cutoffdate,
  3697. );
  3698. $params = array_merge($postidparams, $insertparams);
  3699. if ($CFG->forum_allowforcedreadtracking) {
  3700. $trackingsql = "AND (f.trackingtype = ".FORUM_TRACKING_FORCED."
  3701. OR (f.trackingtype = ".FORUM_TRACKING_OPTIONAL." AND tf.id IS NULL))";
  3702. } else {
  3703. $trackingsql = "AND ((f.trackingtype = ".FORUM_TRACKING_OPTIONAL." OR f.trackingtype = ".FORUM_TRACKING_FORCED.")
  3704. AND tf.id IS NULL)";
  3705. }
  3706. // First insert any new entries.
  3707. $sql = "INSERT INTO {forum_read} (userid, postid, discussionid, forumid, firstread, lastread)
  3708. SELECT :userid1, p.id, p.discussion, d.forum, :firstread, :lastread
  3709. FROM {forum_posts} p
  3710. JOIN {forum_discussions} d ON d.id = p.discussion
  3711. JOIN {forum} f ON f.id = d.forum
  3712. LEFT JOIN {forum_track_prefs} tf ON (tf.userid = :userid2 AND tf.forumid = f.id)
  3713. LEFT JOIN {forum_read} fr ON (
  3714. fr.userid = :userid3
  3715. AND fr.postid = p.id
  3716. AND fr.discussionid = d.id
  3717. AND fr.forumid = f.id
  3718. )
  3719. WHERE p.id $usql
  3720. AND p.modified >= :cutoffdate
  3721. $trackingsql
  3722. AND fr.id IS NULL";
  3723. $status = $DB->execute($sql, $params) && $status;
  3724. // Then update all records.
  3725. $updateparams = array(
  3726. 'userid' => $user->id,
  3727. 'lastread' => $now,
  3728. );
  3729. $params = array_merge($postidparams, $updateparams);
  3730. $status = $DB->set_field_select('forum_read', 'lastread', $now, '
  3731. userid = :userid
  3732. AND lastread <> :lastread
  3733. AND postid ' . $usql,
  3734. $params) && $status;
  3735. return $status;
  3736. }
  3737. /**
  3738. * Mark post as read.
  3739. * @global object
  3740. * @global object
  3741. * @param int $userid
  3742. * @param int $postid
  3743. */
  3744. function forum_tp_add_read_record($userid, $postid) {
  3745. global $CFG, $DB;
  3746. $now = time();
  3747. $cutoffdate = $now - ($CFG->forum_oldpostdays * 24 * 3600);
  3748. if (!$DB->record_exists('forum_read', array('userid' => $userid, 'postid' => $postid))) {
  3749. $sql = "INSERT INTO {forum_read} (userid, postid, discussionid, forumid, firstread, lastread)
  3750. SELECT ?, p.id, p.discussion, d.forum, ?, ?
  3751. FROM {forum_posts} p
  3752. JOIN {forum_discussions} d ON d.id = p.discussion
  3753. WHERE p.id = ? AND p.modified >= ?";
  3754. return $DB->execute($sql, array($userid, $now, $now, $postid, $cutoffdate));
  3755. } else {
  3756. $sql = "UPDATE {forum_read}
  3757. SET lastread = ?
  3758. WHERE userid = ? AND postid = ?";
  3759. return $DB->execute($sql, array($now, $userid, $userid));
  3760. }
  3761. }
  3762. /**
  3763. * If its an old post, do nothing. If the record exists, the maintenance will clear it up later.
  3764. *
  3765. * @param int $userid The ID of the user to mark posts read for.
  3766. * @param object $post The post record for the post to mark as read.
  3767. * @param mixed $unused
  3768. * @return bool
  3769. */
  3770. function forum_tp_mark_post_read($userid, $post, $unused = null) {
  3771. if (!forum_tp_is_post_old($post)) {
  3772. return forum_tp_add_read_record($userid, $post->id);
  3773. } else {
  3774. return true;
  3775. }
  3776. }
  3777. /**
  3778. * Marks a whole forum as read, for a given user
  3779. *
  3780. * @global object
  3781. * @global object
  3782. * @param object $user
  3783. * @param int $forumid
  3784. * @param int|bool $groupid
  3785. * @return bool
  3786. */
  3787. function forum_tp_mark_forum_read($user, $forumid, $groupid=false) {
  3788. global $CFG, $DB;
  3789. $cutoffdate = time() - ($CFG->forum_oldpostdays*24*60*60);
  3790. $groupsel = "";
  3791. $params = array($user->id, $forumid, $cutoffdate);
  3792. if ($groupid !== false) {
  3793. $groupsel = " AND (d.groupid = ? OR d.groupid = -1)";
  3794. $params[] = $groupid;
  3795. }
  3796. $sql = "SELECT p.id
  3797. FROM {forum_posts} p
  3798. LEFT JOIN {forum_discussions} d ON d.id = p.discussion
  3799. LEFT JOIN {forum_read} r ON (r.postid = p.id AND r.userid = ?)
  3800. WHERE d.forum = ?
  3801. AND p.modified >= ? AND r.id is NULL
  3802. $groupsel";
  3803. if ($posts = $DB->get_records_sql($sql, $params)) {
  3804. $postids = array_keys($posts);
  3805. return forum_tp_mark_posts_read($user, $postids);
  3806. }
  3807. return true;
  3808. }
  3809. /**
  3810. * Marks a whole discussion as read, for a given user
  3811. *
  3812. * @global object
  3813. * @global object
  3814. * @param object $user
  3815. * @param int $discussionid
  3816. * @return bool
  3817. */
  3818. function forum_tp_mark_discussion_read($user, $discussionid) {
  3819. global $CFG, $DB;
  3820. $cutoffdate = time() - ($CFG->forum_oldpostdays*24*60*60);
  3821. $sql = "SELECT p.id
  3822. FROM {forum_posts} p
  3823. LEFT JOIN {forum_read} r ON (r.postid = p.id AND r.userid = ?)
  3824. WHERE p.discussion = ?
  3825. AND p.modified >= ? AND r.id is NULL";
  3826. if ($posts = $DB->get_records_sql($sql, array($user->id, $discussionid, $cutoffdate))) {
  3827. $postids = array_keys($posts);
  3828. return forum_tp_mark_posts_read($user, $postids);
  3829. }
  3830. return true;
  3831. }
  3832. /**
  3833. * @global object
  3834. * @param int $userid
  3835. * @param object $post
  3836. */
  3837. function forum_tp_is_post_read($userid, $post) {
  3838. global $DB;
  3839. return (forum_tp_is_post_old($post) ||
  3840. $DB->record_exists('forum_read', array('userid' => $userid, 'postid' => $post->id)));
  3841. }
  3842. /**
  3843. * @global object
  3844. * @param object $post
  3845. * @param int $time Defautls to time()
  3846. */
  3847. function forum_tp_is_post_old($post, $time=null) {
  3848. global $CFG;
  3849. if (is_null($time)) {
  3850. $time = time();
  3851. }
  3852. return ($post->modified < ($time - ($CFG->forum_oldpostdays * 24 * 3600)));
  3853. }
  3854. /**
  3855. * Returns the count of records for the provided user and course.
  3856. * Please note that group access is ignored!
  3857. *
  3858. * @global object
  3859. * @global object
  3860. * @param int $userid
  3861. * @param int $courseid
  3862. * @return array
  3863. */
  3864. function forum_tp_get_course_unread_posts($userid, $courseid) {
  3865. global $CFG, $DB;
  3866. $modinfo = get_fast_modinfo($courseid);
  3867. $forumcms = $modinfo->get_instances_of('forum');
  3868. if (empty($forumcms)) {
  3869. // Return early if the course doesn't have any forum. Will save us a DB query.
  3870. return [];
  3871. }
  3872. $now = floor(time() / MINSECS) * MINSECS; // DB cache friendliness.
  3873. $cutoffdate = $now - ($CFG->forum_oldpostdays * DAYSECS);
  3874. $params = [
  3875. 'privatereplyto' => $userid,
  3876. 'modified' => $cutoffdate,
  3877. 'readuserid' => $userid,
  3878. 'trackprefsuser' => $userid,
  3879. 'courseid' => $courseid,
  3880. 'trackforumuser' => $userid,
  3881. ];
  3882. if (!empty($CFG->forum_enabletimedposts)) {
  3883. $timedsql = "AND d.timestart < :timestart AND (d.timeend = 0 OR d.timeend > :timeend)";
  3884. $params['timestart'] = $now;
  3885. $params['timeend'] = $now;
  3886. } else {
  3887. $timedsql = "";
  3888. }
  3889. if ($CFG->forum_allowforcedreadtracking) {
  3890. $trackingsql = "AND (f.trackingtype = ".FORUM_TRACKING_FORCED."
  3891. OR (f.trackingtype = ".FORUM_TRACKING_OPTIONAL." AND tf.id IS NULL
  3892. AND (SELECT trackforums FROM {user} WHERE id = :trackforumuser) = 1))";
  3893. } else {
  3894. $trackingsql = "AND ((f.trackingtype = ".FORUM_TRACKING_OPTIONAL." OR f.trackingtype = ".FORUM_TRACKING_FORCED.")
  3895. AND tf.id IS NULL
  3896. AND (SELECT trackforums FROM {user} WHERE id = :trackforumuser) = 1)";
  3897. }
  3898. $sql = "SELECT f.id, COUNT(p.id) AS unread,
  3899. COUNT(p.privatereply) as privatereplies,
  3900. COUNT(p.privatereplytouser) as privaterepliestouser
  3901. FROM (
  3902. SELECT
  3903. id,
  3904. discussion,
  3905. CASE WHEN privatereplyto <> 0 THEN 1 END privatereply,
  3906. CASE WHEN privatereplyto = :privatereplyto THEN 1 END privatereplytouser
  3907. FROM {forum_posts}
  3908. WHERE modified >= :modified
  3909. ) p
  3910. JOIN {forum_discussions} d ON d.id = p.discussion
  3911. JOIN {forum} f ON f.id = d.forum
  3912. JOIN {course} c ON c.id = f.course
  3913. LEFT JOIN {forum_read} r ON (r.postid = p.id AND r.userid = :readuserid)
  3914. LEFT JOIN {forum_track_prefs} tf ON (tf.userid = :trackprefsuser AND tf.forumid = f.id)
  3915. WHERE f.course = :courseid
  3916. AND r.id is NULL
  3917. $trackingsql
  3918. $timedsql
  3919. GROUP BY f.id";
  3920. $results = [];
  3921. if ($records = $DB->get_records_sql($sql, $params)) {
  3922. // Loop through each forum instance to check for capability and count the number of unread posts.
  3923. foreach ($forumcms as $cm) {
  3924. // Check that the forum instance exists in the query results.
  3925. if (!isset($records[$cm->instance])) {
  3926. continue;
  3927. }
  3928. $record = $records[$cm->instance];
  3929. $unread = $record->unread;
  3930. // Check if the user has the capability to read private replies for this forum instance.
  3931. $forumcontext = context_module::instance($cm->id);
  3932. if (!has_capability('mod/forum:readprivatereplies', $forumcontext, $userid)) {
  3933. // The real unread count would be the total of unread count minus the number of unread private replies plus
  3934. // the total unread private replies to the user.
  3935. $unread = $record->unread - $record->privatereplies + $record->privaterepliestouser;
  3936. }
  3937. // Build and add the object to the array of results to be returned.
  3938. $results[$record->id] = (object)[
  3939. 'id' => $record->id,
  3940. 'unread' => $unread,
  3941. ];
  3942. }
  3943. }
  3944. return $results;
  3945. }
  3946. /**
  3947. * Returns the count of records for the provided user and forum and [optionally] group.
  3948. *
  3949. * @global object
  3950. * @global object
  3951. * @global object
  3952. * @param object $cm
  3953. * @param object $course
  3954. * @param bool $resetreadcache optional, true to reset the function static $readcache var
  3955. * @return int
  3956. */
  3957. function forum_tp_count_forum_unread_posts($cm, $course, $resetreadcache = false) {
  3958. global $CFG, $USER, $DB;
  3959. static $readcache = array();
  3960. if ($resetreadcache) {
  3961. $readcache = array();
  3962. }
  3963. $forumid = $cm->instance;
  3964. if (!isset($readcache[$course->id])) {
  3965. $readcache[$course->id] = array();
  3966. if ($counts = forum_tp_get_course_unread_posts($USER->id, $course->id)) {
  3967. foreach ($counts as $count) {
  3968. $readcache[$course->id][$count->id] = $count->unread;
  3969. }
  3970. }
  3971. }
  3972. if (empty($readcache[$course->id][$forumid])) {
  3973. // no need to check group mode ;-)
  3974. return 0;
  3975. }
  3976. $groupmode = groups_get_activity_groupmode($cm, $course);
  3977. if ($groupmode != SEPARATEGROUPS) {
  3978. return $readcache[$course->id][$forumid];
  3979. }
  3980. $forumcontext = context_module::instance($cm->id);
  3981. if (has_any_capability(['moodle/site:accessallgroups', 'mod/forum:readprivatereplies'], $forumcontext)) {
  3982. return $readcache[$course->id][$forumid];
  3983. }
  3984. require_once($CFG->dirroot.'/course/lib.php');
  3985. $modinfo = get_fast_modinfo($course);
  3986. $mygroups = $modinfo->get_groups($cm->groupingid);
  3987. // add all groups posts
  3988. $mygroups[-1] = -1;
  3989. list ($groupssql, $groupsparams) = $DB->get_in_or_equal($mygroups, SQL_PARAMS_NAMED);
  3990. $now = floor(time() / MINSECS) * MINSECS; // DB Cache friendliness.
  3991. $cutoffdate = $now - ($CFG->forum_oldpostdays * DAYSECS);
  3992. $params = [
  3993. 'readuser' => $USER->id,
  3994. 'forum' => $forumid,
  3995. 'cutoffdate' => $cutoffdate,
  3996. 'privatereplyto' => $USER->id,
  3997. ];
  3998. if (!empty($CFG->forum_enabletimedposts)) {
  3999. $timedsql = "AND d.timestart < :timestart AND (d.timeend = 0 OR d.timeend > :timeend)";
  4000. $params['timestart'] = $now;
  4001. $params['timeend'] = $now;
  4002. } else {
  4003. $timedsql = "";
  4004. }
  4005. $params = array_merge($params, $groupsparams);
  4006. $sql = "SELECT COUNT(p.id)
  4007. FROM {forum_posts} p
  4008. JOIN {forum_discussions} d ON p.discussion = d.id
  4009. LEFT JOIN {forum_read} r ON (r.postid = p.id AND r.userid = :readuser)
  4010. WHERE d.forum = :forum
  4011. AND p.modified >= :cutoffdate AND r.id is NULL
  4012. $timedsql
  4013. AND d.groupid $groupssql
  4014. AND (p.privatereplyto = 0 OR p.privatereplyto = :privatereplyto)";
  4015. return $DB->get_field_sql($sql, $params);
  4016. }
  4017. /**
  4018. * Deletes read records for the specified index. At least one parameter must be specified.
  4019. *
  4020. * @global object
  4021. * @param int $userid
  4022. * @param int $postid
  4023. * @param int $discussionid
  4024. * @param int $forumid
  4025. * @return bool
  4026. */
  4027. function forum_tp_delete_read_records($userid=-1, $postid=-1, $discussionid=-1, $forumid=-1) {
  4028. global $DB;
  4029. $params = array();
  4030. $select = '';
  4031. if ($userid > -1) {
  4032. if ($select != '') $select .= ' AND ';
  4033. $select .= 'userid = ?';
  4034. $params[] = $userid;
  4035. }
  4036. if ($postid > -1) {
  4037. if ($select != '') $select .= ' AND ';
  4038. $select .= 'postid = ?';
  4039. $params[] = $postid;
  4040. }
  4041. if ($discussionid > -1) {
  4042. if ($select != '') $select .= ' AND ';
  4043. $select .= 'discussionid = ?';
  4044. $params[] = $discussionid;
  4045. }
  4046. if ($forumid > -1) {
  4047. if ($select != '') $select .= ' AND ';
  4048. $select .= 'forumid = ?';
  4049. $params[] = $forumid;
  4050. }
  4051. if ($select == '') {
  4052. return false;
  4053. }
  4054. else {
  4055. return $DB->delete_records_select('forum_read', $select, $params);
  4056. }
  4057. }
  4058. /**
  4059. * Get a list of forums not tracked by the user.
  4060. *
  4061. * @global object
  4062. * @global object
  4063. * @param int $userid The id of the user to use.
  4064. * @param int $courseid The id of the course being checked.
  4065. * @return mixed An array indexed by forum id, or false.
  4066. */
  4067. function forum_tp_get_untracked_forums($userid, $courseid) {
  4068. global $CFG, $DB;
  4069. if ($CFG->forum_allowforcedreadtracking) {
  4070. $trackingsql = "AND (f.trackingtype = ".FORUM_TRACKING_OFF."
  4071. OR (f.trackingtype = ".FORUM_TRACKING_OPTIONAL." AND (ft.id IS NOT NULL
  4072. OR (SELECT trackforums FROM {user} WHERE id = ?) = 0)))";
  4073. } else {
  4074. $trackingsql = "AND (f.trackingtype = ".FORUM_TRACKING_OFF."
  4075. OR ((f.trackingtype = ".FORUM_TRACKING_OPTIONAL." OR f.trackingtype = ".FORUM_TRACKING_FORCED.")
  4076. AND (ft.id IS NOT NULL
  4077. OR (SELECT trackforums FROM {user} WHERE id = ?) = 0)))";
  4078. }
  4079. $sql = "SELECT f.id
  4080. FROM {forum} f
  4081. LEFT JOIN {forum_track_prefs} ft ON (ft.forumid = f.id AND ft.userid = ?)
  4082. WHERE f.course = ?
  4083. $trackingsql";
  4084. if ($forums = $DB->get_records_sql($sql, array($userid, $courseid, $userid))) {
  4085. foreach ($forums as $forum) {
  4086. $forums[$forum->id] = $forum;
  4087. }
  4088. return $forums;
  4089. } else {
  4090. return array();
  4091. }
  4092. }
  4093. /**
  4094. * Determine if a user can track forums and optionally a particular forum.
  4095. * Checks the site settings, the user settings and the forum settings (if
  4096. * requested).
  4097. *
  4098. * @global object
  4099. * @global object
  4100. * @global object
  4101. * @param mixed $forum The forum object to test, or the int id (optional).
  4102. * @param mixed $userid The user object to check for (optional).
  4103. * @return boolean
  4104. */
  4105. function forum_tp_can_track_forums($forum=false, $user=false) {
  4106. global $USER, $CFG, $DB;
  4107. // if possible, avoid expensive
  4108. // queries
  4109. if (empty($CFG->forum_trackreadposts)) {
  4110. return false;
  4111. }
  4112. if ($user === false) {
  4113. $user = $USER;
  4114. }
  4115. if (isguestuser($user) or empty($user->id)) {
  4116. return false;
  4117. }
  4118. if ($forum === false) {
  4119. if ($CFG->forum_allowforcedreadtracking) {
  4120. // Since we can force tracking, assume yes without a specific forum.
  4121. return true;
  4122. } else {
  4123. return (bool)$user->trackforums;
  4124. }
  4125. }
  4126. // Work toward always passing an object...
  4127. if (is_numeric($forum)) {
  4128. debugging('Better use proper forum object.', DEBUG_DEVELOPER);
  4129. $forum = $DB->get_record('forum', array('id' => $forum), '', 'id,trackingtype');
  4130. }
  4131. $forumallows = ($forum->trackingtype == FORUM_TRACKING_OPTIONAL);
  4132. $forumforced = ($forum->trackingtype == FORUM_TRACKING_FORCED);
  4133. if ($CFG->forum_allowforcedreadtracking) {
  4134. // If we allow forcing, then forced forums takes procidence over user setting.
  4135. return ($forumforced || ($forumallows && (!empty($user->trackforums) && (bool)$user->trackforums)));
  4136. } else {
  4137. // If we don't allow forcing, user setting trumps.
  4138. return ($forumforced || $forumallows) && !empty($user->trackforums);
  4139. }
  4140. }
  4141. /**
  4142. * Tells whether a specific forum is tracked by the user. A user can optionally
  4143. * be specified. If not specified, the current user is assumed.
  4144. *
  4145. * @global object
  4146. * @global object
  4147. * @global object
  4148. * @param mixed $forum If int, the id of the forum being checked; if object, the forum object
  4149. * @param int $userid The id of the user being checked (optional).
  4150. * @return boolean
  4151. */
  4152. function forum_tp_is_tracked($forum, $user=false) {
  4153. global $USER, $CFG, $DB;
  4154. if ($user === false) {
  4155. $user = $USER;
  4156. }
  4157. if (isguestuser($user) or empty($user->id)) {
  4158. return false;
  4159. }
  4160. $cache = cache::make('mod_forum', 'forum_is_tracked');
  4161. $forumid = is_numeric($forum) ? $forum : $forum->id;
  4162. $key = $forumid . '_' . $user->id;
  4163. if ($cachedvalue = $cache->get($key)) {
  4164. return $cachedvalue == 'tracked';
  4165. }
  4166. // Work toward always passing an object...
  4167. if (is_numeric($forum)) {
  4168. debugging('Better use proper forum object.', DEBUG_DEVELOPER);
  4169. $forum = $DB->get_record('forum', array('id' => $forum));
  4170. }
  4171. if (!forum_tp_can_track_forums($forum, $user)) {
  4172. return false;
  4173. }
  4174. $forumallows = ($forum->trackingtype == FORUM_TRACKING_OPTIONAL);
  4175. $forumforced = ($forum->trackingtype == FORUM_TRACKING_FORCED);
  4176. $userpref = $DB->get_record('forum_track_prefs', array('userid' => $user->id, 'forumid' => $forum->id));
  4177. if ($CFG->forum_allowforcedreadtracking) {
  4178. $istracked = $forumforced || ($forumallows && $userpref === false);
  4179. } else {
  4180. $istracked = ($forumallows || $forumforced) && $userpref === false;
  4181. }
  4182. // We have to store a string here because the cache API returns false
  4183. // when it can't find the key which would be confused with our legitimate
  4184. // false value. *sigh*.
  4185. $cache->set($key, $istracked ? 'tracked' : 'not');
  4186. return $istracked;
  4187. }
  4188. /**
  4189. * @global object
  4190. * @global object
  4191. * @param int $forumid
  4192. * @param int $userid
  4193. */
  4194. function forum_tp_start_tracking($forumid, $userid=false) {
  4195. global $USER, $DB;
  4196. if ($userid === false) {
  4197. $userid = $USER->id;
  4198. }
  4199. return $DB->delete_records('forum_track_prefs', array('userid' => $userid, 'forumid' => $forumid));
  4200. }
  4201. /**
  4202. * @global object
  4203. * @global object
  4204. * @param int $forumid
  4205. * @param int $userid
  4206. */
  4207. function forum_tp_stop_tracking($forumid, $userid=false) {
  4208. global $USER, $DB;
  4209. if ($userid === false) {
  4210. $userid = $USER->id;
  4211. }
  4212. if (!$DB->record_exists('forum_track_prefs', array('userid' => $userid, 'forumid' => $forumid))) {
  4213. $track_prefs = new stdClass();
  4214. $track_prefs->userid = $userid;
  4215. $track_prefs->forumid = $forumid;
  4216. $DB->insert_record('forum_track_prefs', $track_prefs);
  4217. }
  4218. return forum_tp_delete_read_records($userid, -1, -1, $forumid);
  4219. }
  4220. /**
  4221. * Clean old records from the forum_read table.
  4222. * @global object
  4223. * @global object
  4224. * @return void
  4225. */
  4226. function forum_tp_clean_read_records() {
  4227. global $CFG, $DB;
  4228. if (!isset($CFG->forum_oldpostdays)) {
  4229. return;
  4230. }
  4231. // Look for records older than the cutoffdate that are still in the forum_read table.
  4232. $cutoffdate = time() - ($CFG->forum_oldpostdays*24*60*60);
  4233. //first get the oldest tracking present - we need tis to speedup the next delete query
  4234. $sql = "SELECT MIN(fp.modified) AS first
  4235. FROM {forum_posts} fp
  4236. JOIN {forum_read} fr ON fr.postid=fp.id";
  4237. if (!$first = $DB->get_field_sql($sql)) {
  4238. // nothing to delete;
  4239. return;
  4240. }
  4241. // now delete old tracking info
  4242. $sql = "DELETE
  4243. FROM {forum_read}
  4244. WHERE postid IN (SELECT fp.id
  4245. FROM {forum_posts} fp
  4246. WHERE fp.modified >= ? AND fp.modified < ?)";
  4247. $DB->execute($sql, array($first, $cutoffdate));
  4248. }
  4249. /**
  4250. * Sets the last post for a given discussion
  4251. *
  4252. * @global object
  4253. * @global object
  4254. * @param into $discussionid
  4255. * @return bool|int
  4256. **/
  4257. function forum_discussion_update_last_post($discussionid) {
  4258. global $CFG, $DB;
  4259. // Check the given discussion exists
  4260. if (!$DB->record_exists('forum_discussions', array('id' => $discussionid))) {
  4261. return false;
  4262. }
  4263. // Use SQL to find the last post for this discussion
  4264. $sql = "SELECT id, userid, modified
  4265. FROM {forum_posts}
  4266. WHERE discussion=?
  4267. ORDER BY modified DESC";
  4268. // Lets go find the last post
  4269. if (($lastposts = $DB->get_records_sql($sql, array($discussionid), 0, 1))) {
  4270. $lastpost = reset($lastposts);
  4271. $discussionobject = new stdClass();
  4272. $discussionobject->id = $discussionid;
  4273. $discussionobject->usermodified = $lastpost->userid;
  4274. $discussionobject->timemodified = $lastpost->modified;
  4275. $DB->update_record('forum_discussions', $discussionobject);
  4276. return $lastpost->id;
  4277. }
  4278. // To get here either we couldn't find a post for the discussion (weird)
  4279. // or we couldn't update the discussion record (weird x2)
  4280. return false;
  4281. }
  4282. /**
  4283. * List the actions that correspond to a view of this module.
  4284. * This is used by the participation report.
  4285. *
  4286. * Note: This is not used by new logging system. Event with
  4287. * crud = 'r' and edulevel = LEVEL_PARTICIPATING will
  4288. * be considered as view action.
  4289. *
  4290. * @return array
  4291. */
  4292. function forum_get_view_actions() {
  4293. return array('view discussion', 'search', 'forum', 'forums', 'subscribers', 'view forum');
  4294. }
  4295. /**
  4296. * List the options for forum subscription modes.
  4297. * This is used by the settings page and by the mod_form page.
  4298. *
  4299. * @return array
  4300. */
  4301. function forum_get_subscriptionmode_options() {
  4302. $options = array();
  4303. $options[FORUM_CHOOSESUBSCRIBE] = get_string('subscriptionoptional', 'forum');
  4304. $options[FORUM_FORCESUBSCRIBE] = get_string('subscriptionforced', 'forum');
  4305. $options[FORUM_INITIALSUBSCRIBE] = get_string('subscriptionauto', 'forum');
  4306. $options[FORUM_DISALLOWSUBSCRIBE] = get_string('subscriptiondisabled', 'forum');
  4307. return $options;
  4308. }
  4309. /**
  4310. * List the actions that correspond to a post of this module.
  4311. * This is used by the participation report.
  4312. *
  4313. * Note: This is not used by new logging system. Event with
  4314. * crud = ('c' || 'u' || 'd') and edulevel = LEVEL_PARTICIPATING
  4315. * will be considered as post action.
  4316. *
  4317. * @return array
  4318. */
  4319. function forum_get_post_actions() {
  4320. return array('add discussion','add post','delete discussion','delete post','move discussion','prune post','update post');
  4321. }
  4322. /**
  4323. * Returns a warning object if a user has reached the number of posts equal to
  4324. * the warning/blocking setting, or false if there is no warning to show.
  4325. *
  4326. * @param int|stdClass $forum the forum id or the forum object
  4327. * @param stdClass $cm the course module
  4328. * @return stdClass|bool returns an object with the warning information, else
  4329. * returns false if no warning is required.
  4330. */
  4331. function forum_check_throttling($forum, $cm = null) {
  4332. global $CFG, $DB, $USER;
  4333. if (is_numeric($forum)) {
  4334. $forum = $DB->get_record('forum', array('id' => $forum), '*', MUST_EXIST);
  4335. }
  4336. if (!is_object($forum)) {
  4337. return false; // This is broken.
  4338. }
  4339. if (!$cm) {
  4340. $cm = get_coursemodule_from_instance('forum', $forum->id, $forum->course, false, MUST_EXIST);
  4341. }
  4342. if (empty($forum->blockafter)) {
  4343. return false;
  4344. }
  4345. if (empty($forum->blockperiod)) {
  4346. return false;
  4347. }
  4348. $modcontext = context_module::instance($cm->id);
  4349. if (has_capability('mod/forum:postwithoutthrottling', $modcontext)) {
  4350. return false;
  4351. }
  4352. // Get the number of posts in the last period we care about.
  4353. $timenow = time();
  4354. $timeafter = $timenow - $forum->blockperiod;
  4355. $numposts = $DB->count_records_sql('SELECT COUNT(p.id) FROM {forum_posts} p
  4356. JOIN {forum_discussions} d
  4357. ON p.discussion = d.id WHERE d.forum = ?
  4358. AND p.userid = ? AND p.created > ?', array($forum->id, $USER->id, $timeafter));
  4359. $a = new stdClass();
  4360. $a->blockafter = $forum->blockafter;
  4361. $a->numposts = $numposts;
  4362. $a->blockperiod = get_string('secondstotime'.$forum->blockperiod);
  4363. if ($forum->blockafter <= $numposts) {
  4364. $warning = new stdClass();
  4365. $warning->canpost = false;
  4366. $warning->errorcode = 'forumblockingtoomanyposts';
  4367. $warning->module = 'error';
  4368. $warning->additional = $a;
  4369. $warning->link = $CFG->wwwroot . '/mod/forum/view.php?f=' . $forum->id;
  4370. return $warning;
  4371. }
  4372. if ($forum->warnafter <= $numposts) {
  4373. $warning = new stdClass();
  4374. $warning->canpost = true;
  4375. $warning->errorcode = 'forumblockingalmosttoomanyposts';
  4376. $warning->module = 'forum';
  4377. $warning->additional = $a;
  4378. $warning->link = null;
  4379. return $warning;
  4380. }
  4381. }
  4382. /**
  4383. * Throws an error if the user is no longer allowed to post due to having reached
  4384. * or exceeded the number of posts specified in 'Post threshold for blocking'
  4385. * setting.
  4386. *
  4387. * @since Moodle 2.5
  4388. * @param stdClass $thresholdwarning the warning information returned
  4389. * from the function forum_check_throttling.
  4390. */
  4391. function forum_check_blocking_threshold($thresholdwarning) {
  4392. if (!empty($thresholdwarning) && !$thresholdwarning->canpost) {
  4393. print_error($thresholdwarning->errorcode,
  4394. $thresholdwarning->module,
  4395. $thresholdwarning->link,
  4396. $thresholdwarning->additional);
  4397. }
  4398. }
  4399. /**
  4400. * Removes all grades from gradebook
  4401. *
  4402. * @global object
  4403. * @global object
  4404. * @param int $courseid
  4405. * @param string $type optional
  4406. */
  4407. function forum_reset_gradebook($courseid, $type='') {
  4408. global $CFG, $DB;
  4409. $wheresql = '';
  4410. $params = array($courseid);
  4411. if ($type) {
  4412. $wheresql = "AND f.type=?";
  4413. $params[] = $type;
  4414. }
  4415. $sql = "SELECT f.*, cm.idnumber as cmidnumber, f.course as courseid
  4416. FROM {forum} f, {course_modules} cm, {modules} m
  4417. WHERE m.name='forum' AND m.id=cm.module AND cm.instance=f.id AND f.course=? $wheresql";
  4418. if ($forums = $DB->get_records_sql($sql, $params)) {
  4419. foreach ($forums as $forum) {
  4420. forum_grade_item_update($forum, 'reset', 'reset');
  4421. }
  4422. }
  4423. }
  4424. /**
  4425. * This function is used by the reset_course_userdata function in moodlelib.
  4426. * This function will remove all posts from the specified forum
  4427. * and clean up any related data.
  4428. *
  4429. * @global object
  4430. * @global object
  4431. * @param $data the data submitted from the reset course.
  4432. * @return array status array
  4433. */
  4434. function forum_reset_userdata($data) {
  4435. global $CFG, $DB;
  4436. require_once($CFG->dirroot.'/rating/lib.php');
  4437. $componentstr = get_string('modulenameplural', 'forum');
  4438. $status = array();
  4439. $params = array($data->courseid);
  4440. $removeposts = false;
  4441. $typesql = "";
  4442. if (!empty($data->reset_forum_all)) {
  4443. $removeposts = true;
  4444. $typesstr = get_string('resetforumsall', 'forum');
  4445. $types = array();
  4446. } else if (!empty($data->reset_forum_types)){
  4447. $removeposts = true;
  4448. $types = array();
  4449. $sqltypes = array();
  4450. $forum_types_all = forum_get_forum_types_all();
  4451. foreach ($data->reset_forum_types as $type) {
  4452. if (!array_key_exists($type, $forum_types_all)) {
  4453. continue;
  4454. }
  4455. $types[] = $forum_types_all[$type];
  4456. $sqltypes[] = $type;
  4457. }
  4458. if (!empty($sqltypes)) {
  4459. list($typesql, $typeparams) = $DB->get_in_or_equal($sqltypes);
  4460. $typesql = " AND f.type " . $typesql;
  4461. $params = array_merge($params, $typeparams);
  4462. }
  4463. $typesstr = get_string('resetforums', 'forum').': '.implode(', ', $types);
  4464. }
  4465. $alldiscussionssql = "SELECT fd.id
  4466. FROM {forum_discussions} fd, {forum} f
  4467. WHERE f.course=? AND f.id=fd.forum";
  4468. $allforumssql = "SELECT f.id
  4469. FROM {forum} f
  4470. WHERE f.course=?";
  4471. $allpostssql = "SELECT fp.id
  4472. FROM {forum_posts} fp, {forum_discussions} fd, {forum} f
  4473. WHERE f.course=? AND f.id=fd.forum AND fd.id=fp.discussion";
  4474. $forumssql = $forums = $rm = null;
  4475. // Check if we need to get additional data.
  4476. if ($removeposts || !empty($data->reset_forum_ratings) || !empty($data->reset_forum_tags)) {
  4477. // Set this up if we have to remove ratings.
  4478. $rm = new rating_manager();
  4479. $ratingdeloptions = new stdClass;
  4480. $ratingdeloptions->component = 'mod_forum';
  4481. $ratingdeloptions->ratingarea = 'post';
  4482. // Get the forums for actions that require it.
  4483. $forumssql = "$allforumssql $typesql";
  4484. $forums = $DB->get_records_sql($forumssql, $params);
  4485. }
  4486. if ($removeposts) {
  4487. $discussionssql = "$alldiscussionssql $typesql";
  4488. $postssql = "$allpostssql $typesql";
  4489. // now get rid of all attachments
  4490. $fs = get_file_storage();
  4491. if ($forums) {
  4492. foreach ($forums as $forumid=>$unused) {
  4493. if (!$cm = get_coursemodule_from_instance('forum', $forumid)) {
  4494. continue;
  4495. }
  4496. $context = context_module::instance($cm->id);
  4497. $fs->delete_area_files($context->id, 'mod_forum', 'attachment');
  4498. $fs->delete_area_files($context->id, 'mod_forum', 'post');
  4499. //remove ratings
  4500. $ratingdeloptions->contextid = $context->id;
  4501. $rm->delete_ratings($ratingdeloptions);
  4502. core_tag_tag::delete_instances('mod_forum', null, $context->id);
  4503. }
  4504. }
  4505. // first delete all read flags
  4506. $DB->delete_records_select('forum_read', "forumid IN ($forumssql)", $params);
  4507. // remove tracking prefs
  4508. $DB->delete_records_select('forum_track_prefs', "forumid IN ($forumssql)", $params);
  4509. // remove posts from queue
  4510. $DB->delete_records_select('forum_queue', "discussionid IN ($discussionssql)", $params);
  4511. // all posts - initial posts must be kept in single simple discussion forums
  4512. $DB->delete_records_select('forum_posts', "discussion IN ($discussionssql) AND parent <> 0", $params); // first all children
  4513. $DB->delete_records_select('forum_posts', "discussion IN ($discussionssql AND f.type <> 'single') AND parent = 0", $params); // now the initial posts for non single simple
  4514. // finally all discussions except single simple forums
  4515. $DB->delete_records_select('forum_discussions', "forum IN ($forumssql AND f.type <> 'single')", $params);
  4516. // remove all grades from gradebook
  4517. if (empty($data->reset_gradebook_grades)) {
  4518. if (empty($types)) {
  4519. forum_reset_gradebook($data->courseid);
  4520. } else {
  4521. foreach ($types as $type) {
  4522. forum_reset_gradebook($data->courseid, $type);
  4523. }
  4524. }
  4525. }
  4526. $status[] = array('component'=>$componentstr, 'item'=>$typesstr, 'error'=>false);
  4527. }
  4528. // remove all ratings in this course's forums
  4529. if (!empty($data->reset_forum_ratings)) {
  4530. if ($forums) {
  4531. foreach ($forums as $forumid=>$unused) {
  4532. if (!$cm = get_coursemodule_from_instance('forum', $forumid)) {
  4533. continue;
  4534. }
  4535. $context = context_module::instance($cm->id);
  4536. //remove ratings
  4537. $ratingdeloptions->contextid = $context->id;
  4538. $rm->delete_ratings($ratingdeloptions);
  4539. }
  4540. }
  4541. // remove all grades from gradebook
  4542. if (empty($data->reset_gradebook_grades)) {
  4543. forum_reset_gradebook($data->courseid);
  4544. }
  4545. }
  4546. // Remove all the tags.
  4547. if (!empty($data->reset_forum_tags)) {
  4548. if ($forums) {
  4549. foreach ($forums as $forumid => $unused) {
  4550. if (!$cm = get_coursemodule_from_instance('forum', $forumid)) {
  4551. continue;
  4552. }
  4553. $context = context_module::instance($cm->id);
  4554. core_tag_tag::delete_instances('mod_forum', null, $context->id);
  4555. }
  4556. }
  4557. $status[] = array('component' => $componentstr, 'item' => get_string('tagsdeleted', 'forum'), 'error' => false);
  4558. }
  4559. // remove all digest settings unconditionally - even for users still enrolled in course.
  4560. if (!empty($data->reset_forum_digests)) {
  4561. $DB->delete_records_select('forum_digests', "forum IN ($allforumssql)", $params);
  4562. $status[] = array('component' => $componentstr, 'item' => get_string('resetdigests', 'forum'), 'error' => false);
  4563. }
  4564. // remove all subscriptions unconditionally - even for users still enrolled in course
  4565. if (!empty($data->reset_forum_subscriptions)) {
  4566. $DB->delete_records_select('forum_subscriptions', "forum IN ($allforumssql)", $params);
  4567. $DB->delete_records_select('forum_discussion_subs', "forum IN ($allforumssql)", $params);
  4568. $status[] = array('component' => $componentstr, 'item' => get_string('resetsubscriptions', 'forum'), 'error' => false);
  4569. }
  4570. // remove all tracking prefs unconditionally - even for users still enrolled in course
  4571. if (!empty($data->reset_forum_track_prefs)) {
  4572. $DB->delete_records_select('forum_track_prefs', "forumid IN ($allforumssql)", $params);
  4573. $status[] = array('component'=>$componentstr, 'item'=>get_string('resettrackprefs','forum'), 'error'=>false);
  4574. }
  4575. /// updating dates - shift may be negative too
  4576. if ($data->timeshift) {
  4577. // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
  4578. // See MDL-9367.
  4579. shift_course_mod_dates('forum', array('assesstimestart', 'assesstimefinish'), $data->timeshift, $data->courseid);
  4580. $status[] = array('component'=>$componentstr, 'item'=>get_string('datechanged'), 'error'=>false);
  4581. }
  4582. return $status;
  4583. }
  4584. /**
  4585. * Called by course/reset.php
  4586. *
  4587. * @param $mform form passed by reference
  4588. */
  4589. function forum_reset_course_form_definition(&$mform) {
  4590. $mform->addElement('header', 'forumheader', get_string('modulenameplural', 'forum'));
  4591. $mform->addElement('checkbox', 'reset_forum_all', get_string('resetforumsall','forum'));
  4592. $mform->addElement('select', 'reset_forum_types', get_string('resetforums', 'forum'), forum_get_forum_types_all(), array('multiple' => 'multiple'));
  4593. $mform->setAdvanced('reset_forum_types');
  4594. $mform->disabledIf('reset_forum_types', 'reset_forum_all', 'checked');
  4595. $mform->addElement('checkbox', 'reset_forum_digests', get_string('resetdigests','forum'));
  4596. $mform->setAdvanced('reset_forum_digests');
  4597. $mform->addElement('checkbox', 'reset_forum_subscriptions', get_string('resetsubscriptions','forum'));
  4598. $mform->setAdvanced('reset_forum_subscriptions');
  4599. $mform->addElement('checkbox', 'reset_forum_track_prefs', get_string('resettrackprefs','forum'));
  4600. $mform->setAdvanced('reset_forum_track_prefs');
  4601. $mform->disabledIf('reset_forum_track_prefs', 'reset_forum_all', 'checked');
  4602. $mform->addElement('checkbox', 'reset_forum_ratings', get_string('deleteallratings'));
  4603. $mform->disabledIf('reset_forum_ratings', 'reset_forum_all', 'checked');
  4604. $mform->addElement('checkbox', 'reset_forum_tags', get_string('removeallforumtags', 'forum'));
  4605. $mform->disabledIf('reset_forum_tags', 'reset_forum_all', 'checked');
  4606. }
  4607. /**
  4608. * Course reset form defaults.
  4609. * @return array
  4610. */
  4611. function forum_reset_course_form_defaults($course) {
  4612. return array('reset_forum_all'=>1, 'reset_forum_digests' => 0, 'reset_forum_subscriptions'=>0, 'reset_forum_track_prefs'=>0, 'reset_forum_ratings'=>1);
  4613. }
  4614. /**
  4615. * Returns array of forum layout modes
  4616. *
  4617. * @param bool $useexperimentalui use experimental layout modes or not
  4618. * @return array
  4619. */
  4620. function forum_get_layout_modes(bool $useexperimentalui = false) {
  4621. $modes = [
  4622. FORUM_MODE_FLATOLDEST => get_string('modeflatoldestfirst', 'forum'),
  4623. FORUM_MODE_FLATNEWEST => get_string('modeflatnewestfirst', 'forum'),
  4624. FORUM_MODE_THREADED => get_string('modethreaded', 'forum')
  4625. ];
  4626. if ($useexperimentalui) {
  4627. $modes[FORUM_MODE_NESTED_V2] = get_string('modenestedv2', 'forum');
  4628. } else {
  4629. $modes[FORUM_MODE_NESTED] = get_string('modenested', 'forum');
  4630. }
  4631. return $modes;
  4632. }
  4633. /**
  4634. * Returns array of forum types chooseable on the forum editing form
  4635. *
  4636. * @return array
  4637. */
  4638. function forum_get_forum_types() {
  4639. return array ('general' => get_string('generalforum', 'forum'),
  4640. 'eachuser' => get_string('eachuserforum', 'forum'),
  4641. 'single' => get_string('singleforum', 'forum'),
  4642. 'qanda' => get_string('qandaforum', 'forum'),
  4643. 'blog' => get_string('blogforum', 'forum'));
  4644. }
  4645. /**
  4646. * Returns array of all forum layout modes
  4647. *
  4648. * @return array
  4649. */
  4650. function forum_get_forum_types_all() {
  4651. return array ('news' => get_string('namenews','forum'),
  4652. 'social' => get_string('namesocial','forum'),
  4653. 'general' => get_string('generalforum', 'forum'),
  4654. 'eachuser' => get_string('eachuserforum', 'forum'),
  4655. 'single' => get_string('singleforum', 'forum'),
  4656. 'qanda' => get_string('qandaforum', 'forum'),
  4657. 'blog' => get_string('blogforum', 'forum'));
  4658. }
  4659. /**
  4660. * Returns all other caps used in module
  4661. *
  4662. * @return array
  4663. */
  4664. function forum_get_extra_capabilities() {
  4665. return ['moodle/rating:view', 'moodle/rating:viewany', 'moodle/rating:viewall', 'moodle/rating:rate'];
  4666. }
  4667. /**
  4668. * Adds module specific settings to the settings block
  4669. *
  4670. * @param settings_navigation $settings The settings navigation object
  4671. * @param navigation_node $forumnode The node to add module settings to
  4672. */
  4673. function forum_extend_settings_navigation(settings_navigation $settingsnav, navigation_node $forumnode) {
  4674. global $USER, $PAGE, $CFG, $DB, $OUTPUT;
  4675. if (empty($PAGE->cm->context)) {
  4676. $PAGE->cm->context = context_module::instance($PAGE->cm->instance);
  4677. }
  4678. $vaultfactory = mod_forum\local\container::get_vault_factory();
  4679. $managerfactory = mod_forum\local\container::get_manager_factory();
  4680. $legacydatamapperfactory = mod_forum\local\container::get_legacy_data_mapper_factory();
  4681. $forumvault = $vaultfactory->get_forum_vault();
  4682. $forumentity = $forumvault->get_from_id($PAGE->cm->instance);
  4683. $forumobject = $legacydatamapperfactory->get_forum_data_mapper()->to_legacy_object($forumentity);
  4684. $params = $PAGE->url->params();
  4685. if (!empty($params['d'])) {
  4686. $discussionid = $params['d'];
  4687. }
  4688. // Display all forum reports user has access to.
  4689. if (isloggedin() && !isguestuser()) {
  4690. $reportnames = array_keys(core_component::get_plugin_list('forumreport'));
  4691. foreach ($reportnames as $reportname) {
  4692. if (has_capability("forumreport/{$reportname}:view", $PAGE->cm->context)) {
  4693. $reportlinkparams = [
  4694. 'courseid' => $forumobject->course,
  4695. 'forumid' => $forumobject->id,
  4696. ];
  4697. $reportlink = new moodle_url("/mod/forum/report/{$reportname}/index.php", $reportlinkparams);
  4698. $forumnode->add(get_string('nodetitle', "forumreport_{$reportname}"), $reportlink, navigation_node::TYPE_CONTAINER);
  4699. }
  4700. }
  4701. }
  4702. // For some actions you need to be enrolled, being admin is not enough sometimes here.
  4703. $enrolled = is_enrolled($PAGE->cm->context, $USER, '', false);
  4704. $activeenrolled = is_enrolled($PAGE->cm->context, $USER, '', true);
  4705. $canmanage = has_capability('mod/forum:managesubscriptions', $PAGE->cm->context);
  4706. $subscriptionmode = \mod_forum\subscriptions::get_subscription_mode($forumobject);
  4707. $cansubscribe = $activeenrolled && !\mod_forum\subscriptions::is_forcesubscribed($forumobject) &&
  4708. (!\mod_forum\subscriptions::subscription_disabled($forumobject) || $canmanage);
  4709. if ($canmanage) {
  4710. $mode = $forumnode->add(get_string('subscriptionmode', 'forum'), null, navigation_node::TYPE_CONTAINER);
  4711. $mode->add_class('subscriptionmode');
  4712. // Optional subscription mode.
  4713. $allowchoicestring = get_string('subscriptionoptional', 'forum');
  4714. $allowchoiceaction = new action_link(
  4715. new moodle_url('/mod/forum/subscribe.php', [
  4716. 'id' => $forumobject->id,
  4717. 'mode' => FORUM_CHOOSESUBSCRIBE,
  4718. 'sesskey' => sesskey(),
  4719. ]),
  4720. $allowchoicestring,
  4721. new confirm_action(get_string('subscriptionmodeconfirm', 'mod_forum', $allowchoicestring))
  4722. );
  4723. $allowchoice = $mode->add($allowchoicestring, $allowchoiceaction, navigation_node::TYPE_SETTING);
  4724. // Forced subscription mode.
  4725. $forceforeverstring = get_string('subscriptionforced', 'forum');
  4726. $forceforeveraction = new action_link(
  4727. new moodle_url('/mod/forum/subscribe.php', [
  4728. 'id' => $forumobject->id,
  4729. 'mode' => FORUM_FORCESUBSCRIBE,
  4730. 'sesskey' => sesskey(),
  4731. ]),
  4732. $forceforeverstring,
  4733. new confirm_action(get_string('subscriptionmodeconfirm', 'mod_forum', $forceforeverstring))
  4734. );
  4735. $forceforever = $mode->add($forceforeverstring, $forceforeveraction, navigation_node::TYPE_SETTING);
  4736. // Initial subscription mode.
  4737. $forceinitiallystring = get_string('subscriptionauto', 'forum');
  4738. $forceinitiallyaction = new action_link(
  4739. new moodle_url('/mod/forum/subscribe.php', [
  4740. 'id' => $forumobject->id,
  4741. 'mode' => FORUM_INITIALSUBSCRIBE,
  4742. 'sesskey' => sesskey(),
  4743. ]),
  4744. $forceinitiallystring,
  4745. new confirm_action(get_string('subscriptionmodeconfirm', 'mod_forum', $forceinitiallystring))
  4746. );
  4747. $forceinitially = $mode->add($forceinitiallystring, $forceinitiallyaction, navigation_node::TYPE_SETTING);
  4748. // Disabled subscription mode.
  4749. $disallowchoicestring = get_string('subscriptiondisabled', 'forum');
  4750. $disallowchoiceaction = new action_link(
  4751. new moodle_url('/mod/forum/subscribe.php', [
  4752. 'id' => $forumobject->id,
  4753. 'mode' => FORUM_DISALLOWSUBSCRIBE,
  4754. 'sesskey' => sesskey(),
  4755. ]),
  4756. $disallowchoicestring,
  4757. new confirm_action(get_string('subscriptionmodeconfirm', 'mod_forum', $disallowchoicestring))
  4758. );
  4759. $disallowchoice = $mode->add($disallowchoicestring, $disallowchoiceaction, navigation_node::TYPE_SETTING);
  4760. switch ($subscriptionmode) {
  4761. case FORUM_CHOOSESUBSCRIBE : // 0
  4762. $allowchoice->action = null;
  4763. $allowchoice->add_class('activesetting');
  4764. $allowchoice->icon = new pix_icon('t/selected', '', 'mod_forum');
  4765. break;
  4766. case FORUM_FORCESUBSCRIBE : // 1
  4767. $forceforever->action = null;
  4768. $forceforever->add_class('activesetting');
  4769. $forceforever->icon = new pix_icon('t/selected', '', 'mod_forum');
  4770. break;
  4771. case FORUM_INITIALSUBSCRIBE : // 2
  4772. $forceinitially->action = null;
  4773. $forceinitially->add_class('activesetting');
  4774. $forceinitially->icon = new pix_icon('t/selected', '', 'mod_forum');
  4775. break;
  4776. case FORUM_DISALLOWSUBSCRIBE : // 3
  4777. $disallowchoice->action = null;
  4778. $disallowchoice->add_class('activesetting');
  4779. $disallowchoice->icon = new pix_icon('t/selected', '', 'mod_forum');
  4780. break;
  4781. }
  4782. } else if ($activeenrolled) {
  4783. switch ($subscriptionmode) {
  4784. case FORUM_CHOOSESUBSCRIBE : // 0
  4785. $notenode = $forumnode->add(get_string('subscriptionoptional', 'forum'));
  4786. break;
  4787. case FORUM_FORCESUBSCRIBE : // 1
  4788. $notenode = $forumnode->add(get_string('subscriptionforced', 'forum'));
  4789. break;
  4790. case FORUM_INITIALSUBSCRIBE : // 2
  4791. $notenode = $forumnode->add(get_string('subscriptionauto', 'forum'));
  4792. break;
  4793. case FORUM_DISALLOWSUBSCRIBE : // 3
  4794. $notenode = $forumnode->add(get_string('subscriptiondisabled', 'forum'));
  4795. break;
  4796. }
  4797. }
  4798. if ($cansubscribe) {
  4799. if (\mod_forum\subscriptions::is_subscribed($USER->id, $forumobject, null, $PAGE->cm)) {
  4800. $linktext = get_string('unsubscribe', 'forum');
  4801. } else {
  4802. $linktext = get_string('subscribe', 'forum');
  4803. }
  4804. $url = new moodle_url('/mod/forum/subscribe.php', array('id'=>$forumobject->id, 'sesskey'=>sesskey()));
  4805. $forumnode->add($linktext, $url, navigation_node::TYPE_SETTING);
  4806. if (isset($discussionid)) {
  4807. if (\mod_forum\subscriptions::is_subscribed($USER->id, $forumobject, $discussionid, $PAGE->cm)) {
  4808. $linktext = get_string('unsubscribediscussion', 'forum');
  4809. } else {
  4810. $linktext = get_string('subscribediscussion', 'forum');
  4811. }
  4812. $url = new moodle_url('/mod/forum/subscribe.php', array(
  4813. 'id' => $forumobject->id,
  4814. 'sesskey' => sesskey(),
  4815. 'd' => $discussionid,
  4816. 'returnurl' => $PAGE->url->out(),
  4817. ));
  4818. $forumnode->add($linktext, $url, navigation_node::TYPE_SETTING);
  4819. }
  4820. }
  4821. if (has_capability('mod/forum:viewsubscribers', $PAGE->cm->context)){
  4822. $url = new moodle_url('/mod/forum/subscribers.php', array('id'=>$forumobject->id));
  4823. $forumnode->add(get_string('showsubscribers', 'forum'), $url, navigation_node::TYPE_SETTING);
  4824. }
  4825. if ($enrolled && forum_tp_can_track_forums($forumobject)) { // keep tracking info for users with suspended enrolments
  4826. if ($forumobject->trackingtype == FORUM_TRACKING_OPTIONAL
  4827. || ((!$CFG->forum_allowforcedreadtracking) && $forumobject->trackingtype == FORUM_TRACKING_FORCED)) {
  4828. if (forum_tp_is_tracked($forumobject)) {
  4829. $linktext = get_string('notrackforum', 'forum');
  4830. } else {
  4831. $linktext = get_string('trackforum', 'forum');
  4832. }
  4833. $url = new moodle_url('/mod/forum/settracking.php', array(
  4834. 'id' => $forumobject->id,
  4835. 'sesskey' => sesskey(),
  4836. ));
  4837. $forumnode->add($linktext, $url, navigation_node::TYPE_SETTING);
  4838. }
  4839. }
  4840. if (!isloggedin() && $PAGE->course->id == SITEID) {
  4841. $userid = guest_user()->id;
  4842. } else {
  4843. $userid = $USER->id;
  4844. }
  4845. $hascourseaccess = ($PAGE->course->id == SITEID) || can_access_course($PAGE->course, $userid);
  4846. $enablerssfeeds = !empty($CFG->enablerssfeeds) && !empty($CFG->forum_enablerssfeeds);
  4847. if ($enablerssfeeds && $forumobject->rsstype && $forumobject->rssarticles && $hascourseaccess) {
  4848. if (!function_exists('rss_get_url')) {
  4849. require_once("$CFG->libdir/rsslib.php");
  4850. }
  4851. if ($forumobject->rsstype == 1) {
  4852. $string = get_string('rsssubscriberssdiscussions','forum');
  4853. } else {
  4854. $string = get_string('rsssubscriberssposts','forum');
  4855. }
  4856. $url = new moodle_url(rss_get_url($PAGE->cm->context->id, $userid, "mod_forum", $forumobject->id));
  4857. $forumnode->add($string, $url, settings_navigation::TYPE_SETTING, null, null, new pix_icon('i/rss', ''));
  4858. }
  4859. $capabilitymanager = $managerfactory->get_capability_manager($forumentity);
  4860. if ($capabilitymanager->can_export_forum($USER)) {
  4861. $url = new moodle_url('/mod/forum/export.php', ['id' => $forumobject->id]);
  4862. $forumnode->add(get_string('export', 'mod_forum'), $url, navigation_node::TYPE_SETTING);
  4863. }
  4864. }
  4865. /**
  4866. * Adds information about unread messages, that is only required for the course view page (and
  4867. * similar), to the course-module object.
  4868. * @param cm_info $cm Course-module object
  4869. */
  4870. function forum_cm_info_view(cm_info $cm) {
  4871. global $CFG;
  4872. if (forum_tp_can_track_forums()) {
  4873. if ($unread = forum_tp_count_forum_unread_posts($cm, $cm->get_course())) {
  4874. $out = '<span class="unread"> <a href="' . $cm->url . '#unread">';
  4875. if ($unread == 1) {
  4876. $out .= get_string('unreadpostsone', 'forum');
  4877. } else {
  4878. $out .= get_string('unreadpostsnumber', 'forum', $unread);
  4879. }
  4880. $out .= '</a></span>';
  4881. $cm->set_after_link($out);
  4882. }
  4883. }
  4884. }
  4885. /**
  4886. * Return a list of page types
  4887. * @param string $pagetype current page type
  4888. * @param stdClass $parentcontext Block's parent context
  4889. * @param stdClass $currentcontext Current context of block
  4890. */
  4891. function forum_page_type_list($pagetype, $parentcontext, $currentcontext) {
  4892. $forum_pagetype = array(
  4893. 'mod-forum-*'=>get_string('page-mod-forum-x', 'forum'),
  4894. 'mod-forum-view'=>get_string('page-mod-forum-view', 'forum'),
  4895. 'mod-forum-discuss'=>get_string('page-mod-forum-discuss', 'forum')
  4896. );
  4897. return $forum_pagetype;
  4898. }
  4899. /**
  4900. * Gets all of the courses where the provided user has posted in a forum.
  4901. *
  4902. * @global moodle_database $DB The database connection
  4903. * @param stdClass $user The user who's posts we are looking for
  4904. * @param bool $discussionsonly If true only look for discussions started by the user
  4905. * @param bool $includecontexts If set to trye contexts for the courses will be preloaded
  4906. * @param int $limitfrom The offset of records to return
  4907. * @param int $limitnum The number of records to return
  4908. * @return array An array of courses
  4909. */
  4910. function forum_get_courses_user_posted_in($user, $discussionsonly = false, $includecontexts = true, $limitfrom = null, $limitnum = null) {
  4911. global $DB;
  4912. // If we are only after discussions we need only look at the forum_discussions
  4913. // table and join to the userid there. If we are looking for posts then we need
  4914. // to join to the forum_posts table.
  4915. if (!$discussionsonly) {
  4916. $subquery = "(SELECT DISTINCT fd.course
  4917. FROM {forum_discussions} fd
  4918. JOIN {forum_posts} fp ON fp.discussion = fd.id
  4919. WHERE fp.userid = :userid )";
  4920. } else {
  4921. $subquery= "(SELECT DISTINCT fd.course
  4922. FROM {forum_discussions} fd
  4923. WHERE fd.userid = :userid )";
  4924. }
  4925. $params = array('userid' => $user->id);
  4926. // Join to the context table so that we can preload contexts if required.
  4927. if ($includecontexts) {
  4928. $ctxselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
  4929. $ctxjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
  4930. $params['contextlevel'] = CONTEXT_COURSE;
  4931. } else {
  4932. $ctxselect = '';
  4933. $ctxjoin = '';
  4934. }
  4935. // Now we need to get all of the courses to search.
  4936. // All courses where the user has posted within a forum will be returned.
  4937. $sql = "SELECT c.* $ctxselect
  4938. FROM {course} c
  4939. $ctxjoin
  4940. WHERE c.id IN ($subquery)";
  4941. $courses = $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
  4942. if ($includecontexts) {
  4943. array_map('context_helper::preload_from_record', $courses);
  4944. }
  4945. return $courses;
  4946. }
  4947. /**
  4948. * Gets all of the forums a user has posted in for one or more courses.
  4949. *
  4950. * @global moodle_database $DB
  4951. * @param stdClass $user
  4952. * @param array $courseids An array of courseids to search or if not provided
  4953. * all courses the user has posted within
  4954. * @param bool $discussionsonly If true then only forums where the user has started
  4955. * a discussion will be returned.
  4956. * @param int $limitfrom The offset of records to return
  4957. * @param int $limitnum The number of records to return
  4958. * @return array An array of forums the user has posted within in the provided courses
  4959. */
  4960. function forum_get_forums_user_posted_in($user, array $courseids = null, $discussionsonly = false, $limitfrom = null, $limitnum = null) {
  4961. global $DB;
  4962. if (!is_null($courseids)) {
  4963. list($coursewhere, $params) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED, 'courseid');
  4964. $coursewhere = ' AND f.course '.$coursewhere;
  4965. } else {
  4966. $coursewhere = '';
  4967. $params = array();
  4968. }
  4969. $params['userid'] = $user->id;
  4970. $params['forum'] = 'forum';
  4971. if ($discussionsonly) {
  4972. $join = 'JOIN {forum_discussions} ff ON ff.forum = f.id';
  4973. } else {
  4974. $join = 'JOIN {forum_discussions} fd ON fd.forum = f.id
  4975. JOIN {forum_posts} ff ON ff.discussion = fd.id';
  4976. }
  4977. $sql = "SELECT f.*, cm.id AS cmid
  4978. FROM {forum} f
  4979. JOIN {course_modules} cm ON cm.instance = f.id
  4980. JOIN {modules} m ON m.id = cm.module
  4981. JOIN (
  4982. SELECT f.id
  4983. FROM {forum} f
  4984. {$join}
  4985. WHERE ff.userid = :userid
  4986. GROUP BY f.id
  4987. ) j ON j.id = f.id
  4988. WHERE m.name = :forum
  4989. {$coursewhere}";
  4990. $courseforums = $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
  4991. return $courseforums;
  4992. }
  4993. /**
  4994. * Returns posts made by the selected user in the requested courses.
  4995. *
  4996. * This method can be used to return all of the posts made by the requested user
  4997. * within the given courses.
  4998. * For each course the access of the current user and requested user is checked
  4999. * and then for each post access to the post and forum is checked as well.
  5000. *
  5001. * This function is safe to use with usercapabilities.
  5002. *
  5003. * @global moodle_database $DB
  5004. * @param stdClass $user The user whose posts we want to get
  5005. * @param array $courses The courses to search
  5006. * @param bool $musthaveaccess If set to true errors will be thrown if the user
  5007. * cannot access one or more of the courses to search
  5008. * @param bool $discussionsonly If set to true only discussion starting posts
  5009. * will be returned.
  5010. * @param int $limitfrom The offset of records to return
  5011. * @param int $limitnum The number of records to return
  5012. * @return stdClass An object the following properties
  5013. * ->totalcount: the total number of posts made by the requested user
  5014. * that the current user can see.
  5015. * ->courses: An array of courses the current user can see that the
  5016. * requested user has posted in.
  5017. * ->forums: An array of forums relating to the posts returned in the
  5018. * property below.
  5019. * ->posts: An array containing the posts to show for this request.
  5020. */
  5021. function forum_get_posts_by_user($user, array $courses, $musthaveaccess = false, $discussionsonly = false, $limitfrom = 0, $limitnum = 50) {
  5022. global $DB, $USER, $CFG;
  5023. $return = new stdClass;
  5024. $return->totalcount = 0; // The total number of posts that the current user is able to view
  5025. $return->courses = array(); // The courses the current user can access
  5026. $return->forums = array(); // The forums that the current user can access that contain posts
  5027. $return->posts = array(); // The posts to display
  5028. // First up a small sanity check. If there are no courses to check we can
  5029. // return immediately, there is obviously nothing to search.
  5030. if (empty($courses)) {
  5031. return $return;
  5032. }
  5033. // A couple of quick setups
  5034. $isloggedin = isloggedin();
  5035. $isguestuser = $isloggedin && isguestuser();
  5036. $iscurrentuser = $isloggedin && $USER->id == $user->id;
  5037. // Checkout whether or not the current user has capabilities over the requested
  5038. // user and if so they have the capabilities required to view the requested
  5039. // users content.
  5040. $usercontext = context_user::instance($user->id, MUST_EXIST);
  5041. $hascapsonuser = !$iscurrentuser && $DB->record_exists('role_assignments', array('userid' => $USER->id, 'contextid' => $usercontext->id));
  5042. $hascapsonuser = $hascapsonuser && has_all_capabilities(array('moodle/user:viewdetails', 'moodle/user:readuserposts'), $usercontext);
  5043. // Before we actually search each course we need to check the user's access to the
  5044. // course. If the user doesn't have the appropraite access then we either throw an
  5045. // error if a particular course was requested or we just skip over the course.
  5046. foreach ($courses as $course) {
  5047. $coursecontext = context_course::instance($course->id, MUST_EXIST);
  5048. if ($iscurrentuser || $hascapsonuser) {
  5049. // If it is the current user, or the current user has capabilities to the
  5050. // requested user then all we need to do is check the requested users
  5051. // current access to the course.
  5052. // Note: There is no need to check group access or anything of the like
  5053. // as either the current user is the requested user, or has granted
  5054. // capabilities on the requested user. Either way they can see what the
  5055. // requested user posted, although its VERY unlikely in the `parent` situation
  5056. // that the current user will be able to view the posts in context.
  5057. if (!is_viewing($coursecontext, $user) && !is_enrolled($coursecontext, $user)) {
  5058. // Need to have full access to a course to see the rest of own info
  5059. if ($musthaveaccess) {
  5060. print_error('errorenrolmentrequired', 'forum');
  5061. }
  5062. continue;
  5063. }
  5064. } else {
  5065. // Check whether the current user is enrolled or has access to view the course
  5066. // if they don't we immediately have a problem.
  5067. if (!can_access_course($course)) {
  5068. if ($musthaveaccess) {
  5069. print_error('errorenrolmentrequired', 'forum');
  5070. }
  5071. continue;
  5072. }
  5073. // If groups are in use and enforced throughout the course then make sure
  5074. // we can meet in at least one course level group.
  5075. // Note that we check if either the current user or the requested user have
  5076. // the capability to access all groups. This is because with that capability
  5077. // a user in group A could post in the group B forum. Grrrr.
  5078. if (groups_get_course_groupmode($course) == SEPARATEGROUPS && $course->groupmodeforce
  5079. && !has_capability('moodle/site:accessallgroups', $coursecontext) && !has_capability('moodle/site:accessallgroups', $coursecontext, $user->id)) {
  5080. // If its the guest user to bad... the guest user cannot access groups
  5081. if (!$isloggedin or $isguestuser) {
  5082. // do not use require_login() here because we might have already used require_login($course)
  5083. if ($musthaveaccess) {
  5084. redirect(get_login_url());
  5085. }
  5086. continue;
  5087. }
  5088. // Get the groups of the current user
  5089. $mygroups = array_keys(groups_get_all_groups($course->id, $USER->id, $course->defaultgroupingid, 'g.id, g.name'));
  5090. // Get the groups the requested user is a member of
  5091. $usergroups = array_keys(groups_get_all_groups($course->id, $user->id, $course->defaultgroupingid, 'g.id, g.name'));
  5092. // Check whether they are members of the same group. If they are great.
  5093. $intersect = array_intersect($mygroups, $usergroups);
  5094. if (empty($intersect)) {
  5095. // But they're not... if it was a specific course throw an error otherwise
  5096. // just skip this course so that it is not searched.
  5097. if ($musthaveaccess) {
  5098. print_error("groupnotamember", '', $CFG->wwwroot."/course/view.php?id=$course->id");
  5099. }
  5100. continue;
  5101. }
  5102. }
  5103. }
  5104. // Woo hoo we got this far which means the current user can search this
  5105. // this course for the requested user. Although this is only the course accessibility
  5106. // handling that is complete, the forum accessibility tests are yet to come.
  5107. $return->courses[$course->id] = $course;
  5108. }
  5109. // No longer beed $courses array - lose it not it may be big
  5110. unset($courses);
  5111. // Make sure that we have some courses to search
  5112. if (empty($return->courses)) {
  5113. // If we don't have any courses to search then the reality is that the current
  5114. // user doesn't have access to any courses is which the requested user has posted.
  5115. // Although we do know at this point that the requested user has posts.
  5116. if ($musthaveaccess) {
  5117. print_error('permissiondenied');
  5118. } else {
  5119. return $return;
  5120. }
  5121. }
  5122. // Next step: Collect all of the forums that we will want to search.
  5123. // It is important to note that this step isn't actually about searching, it is
  5124. // about determining which forums we can search by testing accessibility.
  5125. $forums = forum_get_forums_user_posted_in($user, array_keys($return->courses), $discussionsonly);
  5126. // Will be used to build the where conditions for the search
  5127. $forumsearchwhere = array();
  5128. // Will be used to store the where condition params for the search
  5129. $forumsearchparams = array();
  5130. // Will record forums where the user can freely access everything
  5131. $forumsearchfullaccess = array();
  5132. // DB caching friendly
  5133. $now = floor(time() / 60) * 60;
  5134. // For each course to search we want to find the forums the user has posted in
  5135. // and providing the current user can access the forum create a search condition
  5136. // for the forum to get the requested users posts.
  5137. foreach ($return->courses as $course) {
  5138. // Now we need to get the forums
  5139. $modinfo = get_fast_modinfo($course);
  5140. if (empty($modinfo->instances['forum'])) {
  5141. // hmmm, no forums? well at least its easy... skip!
  5142. continue;
  5143. }
  5144. // Iterate
  5145. foreach ($modinfo->get_instances_of('forum') as $forumid => $cm) {
  5146. if (!$cm->uservisible or !isset($forums[$forumid])) {
  5147. continue;
  5148. }
  5149. // Get the forum in question
  5150. $forum = $forums[$forumid];
  5151. // This is needed for functionality later on in the forum code. It is converted to an object
  5152. // because the cm_info is readonly from 2.6. This is a dirty hack because some other parts of the
  5153. // code were expecting an writeable object. See {@link forum_print_post()}.
  5154. $forum->cm = new stdClass();
  5155. foreach ($cm as $key => $value) {
  5156. $forum->cm->$key = $value;
  5157. }
  5158. // Check that either the current user can view the forum, or that the
  5159. // current user has capabilities over the requested user and the requested
  5160. // user can view the discussion
  5161. if (!has_capability('mod/forum:viewdiscussion', $cm->context) && !($hascapsonuser && has_capability('mod/forum:viewdiscussion', $cm->context, $user->id))) {
  5162. continue;
  5163. }
  5164. // This will contain forum specific where clauses
  5165. $forumsearchselect = array();
  5166. if (!$iscurrentuser && !$hascapsonuser) {
  5167. // Make sure we check group access
  5168. if (groups_get_activity_groupmode($cm, $course) == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $cm->context)) {
  5169. $groups = $modinfo->get_groups($cm->groupingid);
  5170. $groups[] = -1;
  5171. list($groupid_sql, $groupid_params) = $DB->get_in_or_equal($groups, SQL_PARAMS_NAMED, 'grps'.$forumid.'_');
  5172. $forumsearchparams = array_merge($forumsearchparams, $groupid_params);
  5173. $forumsearchselect[] = "d.groupid $groupid_sql";
  5174. }
  5175. // hidden timed discussions
  5176. if (!empty($CFG->forum_enabletimedposts) && !has_capability('mod/forum:viewhiddentimedposts', $cm->context)) {
  5177. $forumsearchselect[] = "(d.userid = :userid{$forumid} OR (d.timestart < :timestart{$forumid} AND (d.timeend = 0 OR d.timeend > :timeend{$forumid})))";
  5178. $forumsearchparams['userid'.$forumid] = $user->id;
  5179. $forumsearchparams['timestart'.$forumid] = $now;
  5180. $forumsearchparams['timeend'.$forumid] = $now;
  5181. }
  5182. // qanda access
  5183. if ($forum->type == 'qanda' && !has_capability('mod/forum:viewqandawithoutposting', $cm->context)) {
  5184. // We need to check whether the user has posted in the qanda forum.
  5185. $discussionspostedin = forum_discussions_user_has_posted_in($forum->id, $user->id);
  5186. if (!empty($discussionspostedin)) {
  5187. $forumonlydiscussions = array(); // Holds discussion ids for the discussions the user is allowed to see in this forum.
  5188. foreach ($discussionspostedin as $d) {
  5189. $forumonlydiscussions[] = $d->id;
  5190. }
  5191. list($discussionid_sql, $discussionid_params) = $DB->get_in_or_equal($forumonlydiscussions, SQL_PARAMS_NAMED, 'qanda'.$forumid.'_');
  5192. $forumsearchparams = array_merge($forumsearchparams, $discussionid_params);
  5193. $forumsearchselect[] = "(d.id $discussionid_sql OR p.parent = 0)";
  5194. } else {
  5195. $forumsearchselect[] = "p.parent = 0";
  5196. }
  5197. }
  5198. if (count($forumsearchselect) > 0) {
  5199. $forumsearchwhere[] = "(d.forum = :forum{$forumid} AND ".implode(" AND ", $forumsearchselect).")";
  5200. $forumsearchparams['forum'.$forumid] = $forumid;
  5201. } else {
  5202. $forumsearchfullaccess[] = $forumid;
  5203. }
  5204. } else {
  5205. // The current user/parent can see all of their own posts
  5206. $forumsearchfullaccess[] = $forumid;
  5207. }
  5208. }
  5209. }
  5210. // If we dont have any search conditions, and we don't have any forums where
  5211. // the user has full access then we just return the default.
  5212. if (empty($forumsearchwhere) && empty($forumsearchfullaccess)) {
  5213. return $return;
  5214. }
  5215. // Prepare a where condition for the full access forums.
  5216. if (count($forumsearchfullaccess) > 0) {
  5217. list($fullidsql, $fullidparams) = $DB->get_in_or_equal($forumsearchfullaccess, SQL_PARAMS_NAMED, 'fula');
  5218. $forumsearchparams = array_merge($forumsearchparams, $fullidparams);
  5219. $forumsearchwhere[] = "(d.forum $fullidsql)";
  5220. }
  5221. // Prepare SQL to both count and search.
  5222. // We alias user.id to useridx because we forum_posts already has a userid field and not aliasing this would break
  5223. // oracle and mssql.
  5224. $userfieldsapi = \core_user\fields::for_userpic();
  5225. $userfields = $userfieldsapi->get_sql('u', false, '', 'useridx', false)->selects;
  5226. $countsql = 'SELECT COUNT(*) ';
  5227. $selectsql = 'SELECT p.*, d.forum, d.name AS discussionname, '.$userfields.' ';
  5228. $wheresql = implode(" OR ", $forumsearchwhere);
  5229. if ($discussionsonly) {
  5230. if ($wheresql == '') {
  5231. $wheresql = 'p.parent = 0';
  5232. } else {
  5233. $wheresql = 'p.parent = 0 AND ('.$wheresql.')';
  5234. }
  5235. }
  5236. $sql = "FROM {forum_posts} p
  5237. JOIN {forum_discussions} d ON d.id = p.discussion
  5238. JOIN {user} u ON u.id = p.userid
  5239. WHERE ($wheresql)
  5240. AND p.userid = :userid ";
  5241. $orderby = "ORDER BY p.modified DESC";
  5242. $forumsearchparams['userid'] = $user->id;
  5243. // Set the total number posts made by the requested user that the current user can see
  5244. $return->totalcount = $DB->count_records_sql($countsql.$sql, $forumsearchparams);
  5245. // Set the collection of posts that has been requested
  5246. $return->posts = $DB->get_records_sql($selectsql.$sql.$orderby, $forumsearchparams, $limitfrom, $limitnum);
  5247. // We need to build an array of forums for which posts will be displayed.
  5248. // We do this here to save the caller needing to retrieve them themselves before
  5249. // printing these forums posts. Given we have the forums already there is
  5250. // practically no overhead here.
  5251. foreach ($return->posts as $post) {
  5252. if (!array_key_exists($post->forum, $return->forums)) {
  5253. $return->forums[$post->forum] = $forums[$post->forum];
  5254. }
  5255. }
  5256. return $return;
  5257. }
  5258. /**
  5259. * Set the per-forum maildigest option for the specified user.
  5260. *
  5261. * @param stdClass $forum The forum to set the option for.
  5262. * @param int $maildigest The maildigest option.
  5263. * @param stdClass $user The user object. This defaults to the global $USER object.
  5264. * @throws invalid_digest_setting thrown if an invalid maildigest option is provided.
  5265. */
  5266. function forum_set_user_maildigest($forum, $maildigest, $user = null) {
  5267. global $DB, $USER;
  5268. if (is_number($forum)) {
  5269. $forum = $DB->get_record('forum', array('id' => $forum));
  5270. }
  5271. if ($user === null) {
  5272. $user = $USER;
  5273. }
  5274. $course = $DB->get_record('course', array('id' => $forum->course), '*', MUST_EXIST);
  5275. $cm = get_coursemodule_from_instance('forum', $forum->id, $course->id, false, MUST_EXIST);
  5276. $context = context_module::instance($cm->id);
  5277. // User must be allowed to see this forum.
  5278. require_capability('mod/forum:viewdiscussion', $context, $user->id);
  5279. // Validate the maildigest setting.
  5280. $digestoptions = forum_get_user_digest_options($user);
  5281. if (!isset($digestoptions[$maildigest])) {
  5282. throw new moodle_exception('invaliddigestsetting', 'mod_forum');
  5283. }
  5284. // Attempt to retrieve any existing forum digest record.
  5285. $subscription = $DB->get_record('forum_digests', array(
  5286. 'userid' => $user->id,
  5287. 'forum' => $forum->id,
  5288. ));
  5289. // Create or Update the existing maildigest setting.
  5290. if ($subscription) {
  5291. if ($maildigest == -1) {
  5292. $DB->delete_records('forum_digests', array('forum' => $forum->id, 'userid' => $user->id));
  5293. } else if ($maildigest !== $subscription->maildigest) {
  5294. // Only update the maildigest setting if it's changed.
  5295. $subscription->maildigest = $maildigest;
  5296. $DB->update_record('forum_digests', $subscription);
  5297. }
  5298. } else {
  5299. if ($maildigest != -1) {
  5300. // Only insert the maildigest setting if it's non-default.
  5301. $subscription = new stdClass();
  5302. $subscription->forum = $forum->id;
  5303. $subscription->userid = $user->id;
  5304. $subscription->maildigest = $maildigest;
  5305. $subscription->id = $DB->insert_record('forum_digests', $subscription);
  5306. }
  5307. }
  5308. }
  5309. /**
  5310. * Determine the maildigest setting for the specified user against the
  5311. * specified forum.
  5312. *
  5313. * @param Array $digests An array of forums and user digest settings.
  5314. * @param stdClass $user The user object containing the id and maildigest default.
  5315. * @param int $forumid The ID of the forum to check.
  5316. * @return int The calculated maildigest setting for this user and forum.
  5317. */
  5318. function forum_get_user_maildigest_bulk($digests, $user, $forumid) {
  5319. if (isset($digests[$forumid]) && isset($digests[$forumid][$user->id])) {
  5320. $maildigest = $digests[$forumid][$user->id];
  5321. if ($maildigest === -1) {
  5322. $maildigest = $user->maildigest;
  5323. }
  5324. } else {
  5325. $maildigest = $user->maildigest;
  5326. }
  5327. return $maildigest;
  5328. }
  5329. /**
  5330. * Retrieve the list of available user digest options.
  5331. *
  5332. * @param stdClass $user The user object. This defaults to the global $USER object.
  5333. * @return array The mapping of values to digest options.
  5334. */
  5335. function forum_get_user_digest_options($user = null) {
  5336. global $USER;
  5337. // Revert to the global user object.
  5338. if ($user === null) {
  5339. $user = $USER;
  5340. }
  5341. $digestoptions = array();
  5342. $digestoptions['0'] = get_string('emaildigestoffshort', 'mod_forum');
  5343. $digestoptions['1'] = get_string('emaildigestcompleteshort', 'mod_forum');
  5344. $digestoptions['2'] = get_string('emaildigestsubjectsshort', 'mod_forum');
  5345. // We need to add the default digest option at the end - it relies on
  5346. // the contents of the existing values.
  5347. $digestoptions['-1'] = get_string('emaildigestdefault', 'mod_forum',
  5348. $digestoptions[$user->maildigest]);
  5349. // Resort the options to be in a sensible order.
  5350. ksort($digestoptions);
  5351. return $digestoptions;
  5352. }
  5353. /**
  5354. * Determine the current context if one was not already specified.
  5355. *
  5356. * If a context of type context_module is specified, it is immediately
  5357. * returned and not checked.
  5358. *
  5359. * @param int $forumid The ID of the forum
  5360. * @param context_module $context The current context.
  5361. * @return context_module The context determined
  5362. */
  5363. function forum_get_context($forumid, $context = null) {
  5364. global $PAGE;
  5365. if (!$context || !($context instanceof context_module)) {
  5366. // Find out forum context. First try to take current page context to save on DB query.
  5367. if ($PAGE->cm && $PAGE->cm->modname === 'forum' && $PAGE->cm->instance == $forumid
  5368. && $PAGE->context->contextlevel == CONTEXT_MODULE && $PAGE->context->instanceid == $PAGE->cm->id) {
  5369. $context = $PAGE->context;
  5370. } else {
  5371. $cm = get_coursemodule_from_instance('forum', $forumid);
  5372. $context = \context_module::instance($cm->id);
  5373. }
  5374. }
  5375. return $context;
  5376. }
  5377. /**
  5378. * Mark the activity completed (if required) and trigger the course_module_viewed event.
  5379. *
  5380. * @param stdClass $forum forum object
  5381. * @param stdClass $course course object
  5382. * @param stdClass $cm course module object
  5383. * @param stdClass $context context object
  5384. * @since Moodle 2.9
  5385. */
  5386. function forum_view($forum, $course, $cm, $context) {
  5387. // Completion.
  5388. $completion = new completion_info($course);
  5389. $completion->set_module_viewed($cm);
  5390. // Trigger course_module_viewed event.
  5391. $params = array(
  5392. 'context' => $context,
  5393. 'objectid' => $forum->id
  5394. );
  5395. $event = \mod_forum\event\course_module_viewed::create($params);
  5396. $event->add_record_snapshot('course_modules', $cm);
  5397. $event->add_record_snapshot('course', $course);
  5398. $event->add_record_snapshot('forum', $forum);
  5399. $event->trigger();
  5400. }
  5401. /**
  5402. * Trigger the discussion viewed event
  5403. *
  5404. * @param stdClass $modcontext module context object
  5405. * @param stdClass $forum forum object
  5406. * @param stdClass $discussion discussion object
  5407. * @since Moodle 2.9
  5408. */
  5409. function forum_discussion_view($modcontext, $forum, $discussion) {
  5410. $params = array(
  5411. 'context' => $modcontext,
  5412. 'objectid' => $discussion->id,
  5413. );
  5414. $event = \mod_forum\event\discussion_viewed::create($params);
  5415. $event->add_record_snapshot('forum_discussions', $discussion);
  5416. $event->add_record_snapshot('forum', $forum);
  5417. $event->trigger();
  5418. }
  5419. /**
  5420. * Set the discussion to pinned and trigger the discussion pinned event
  5421. *
  5422. * @param stdClass $modcontext module context object
  5423. * @param stdClass $forum forum object
  5424. * @param stdClass $discussion discussion object
  5425. * @since Moodle 3.1
  5426. */
  5427. function forum_discussion_pin($modcontext, $forum, $discussion) {
  5428. global $DB;
  5429. $DB->set_field('forum_discussions', 'pinned', FORUM_DISCUSSION_PINNED, array('id' => $discussion->id));
  5430. $params = array(
  5431. 'context' => $modcontext,
  5432. 'objectid' => $discussion->id,
  5433. 'other' => array('forumid' => $forum->id)
  5434. );
  5435. $event = \mod_forum\event\discussion_pinned::create($params);
  5436. $event->add_record_snapshot('forum_discussions', $discussion);
  5437. $event->trigger();
  5438. }
  5439. /**
  5440. * Set discussion to unpinned and trigger the discussion unpin event
  5441. *
  5442. * @param stdClass $modcontext module context object
  5443. * @param stdClass $forum forum object
  5444. * @param stdClass $discussion discussion object
  5445. * @since Moodle 3.1
  5446. */
  5447. function forum_discussion_unpin($modcontext, $forum, $discussion) {
  5448. global $DB;
  5449. $DB->set_field('forum_discussions', 'pinned', FORUM_DISCUSSION_UNPINNED, array('id' => $discussion->id));
  5450. $params = array(
  5451. 'context' => $modcontext,
  5452. 'objectid' => $discussion->id,
  5453. 'other' => array('forumid' => $forum->id)
  5454. );
  5455. $event = \mod_forum\event\discussion_unpinned::create($params);
  5456. $event->add_record_snapshot('forum_discussions', $discussion);
  5457. $event->trigger();
  5458. }
  5459. /**
  5460. * Add nodes to myprofile page.
  5461. *
  5462. * @param \core_user\output\myprofile\tree $tree Tree object
  5463. * @param stdClass $user user object
  5464. * @param bool $iscurrentuser
  5465. * @param stdClass $course Course object
  5466. *
  5467. * @return bool
  5468. */
  5469. function mod_forum_myprofile_navigation(core_user\output\myprofile\tree $tree, $user, $iscurrentuser, $course) {
  5470. if (isguestuser($user)) {
  5471. // The guest user cannot post, so it is not possible to view any posts.
  5472. // May as well just bail aggressively here.
  5473. return false;
  5474. }
  5475. $postsurl = new moodle_url('/mod/forum/user.php', array('id' => $user->id));
  5476. if (!empty($course)) {
  5477. $postsurl->param('course', $course->id);
  5478. }
  5479. $string = get_string('forumposts', 'mod_forum');
  5480. $node = new core_user\output\myprofile\node('miscellaneous', 'forumposts', $string, null, $postsurl);
  5481. $tree->add_node($node);
  5482. $discussionssurl = new moodle_url('/mod/forum/user.php', array('id' => $user->id, 'mode' => 'discussions'));
  5483. if (!empty($course)) {
  5484. $discussionssurl->param('course', $course->id);
  5485. }
  5486. $string = get_string('myprofileotherdis', 'mod_forum');
  5487. $node = new core_user\output\myprofile\node('miscellaneous', 'forumdiscussions', $string, null,
  5488. $discussionssurl);
  5489. $tree->add_node($node);
  5490. return true;
  5491. }
  5492. /**
  5493. * Checks whether the author's name and picture for a given post should be hidden or not.
  5494. *
  5495. * @param object $post The forum post.
  5496. * @param object $forum The forum object.
  5497. * @return bool
  5498. * @throws coding_exception
  5499. */
  5500. function forum_is_author_hidden($post, $forum) {
  5501. if (!isset($post->parent)) {
  5502. throw new coding_exception('$post->parent must be set.');
  5503. }
  5504. if (!isset($forum->type)) {
  5505. throw new coding_exception('$forum->type must be set.');
  5506. }
  5507. if ($forum->type === 'single' && empty($post->parent)) {
  5508. return true;
  5509. }
  5510. return false;
  5511. }
  5512. /**
  5513. * Manage inplace editable saves.
  5514. *
  5515. * @param string $itemtype The type of item.
  5516. * @param int $itemid The ID of the item.
  5517. * @param mixed $newvalue The new value
  5518. * @return string
  5519. */
  5520. function mod_forum_inplace_editable($itemtype, $itemid, $newvalue) {
  5521. global $DB, $PAGE;
  5522. if ($itemtype === 'digestoptions') {
  5523. // The itemid is the forumid.
  5524. $forum = $DB->get_record('forum', array('id' => $itemid), '*', MUST_EXIST);
  5525. $course = $DB->get_record('course', array('id' => $forum->course), '*', MUST_EXIST);
  5526. $cm = get_coursemodule_from_instance('forum', $forum->id, $course->id, false, MUST_EXIST);
  5527. $context = context_module::instance($cm->id);
  5528. $PAGE->set_context($context);
  5529. require_login($course, false, $cm);
  5530. forum_set_user_maildigest($forum, $newvalue);
  5531. $renderer = $PAGE->get_renderer('mod_forum');
  5532. return $renderer->render_digest_options($forum, $newvalue);
  5533. }
  5534. }
  5535. /**
  5536. * Determine whether the specified forum's cutoff date is reached.
  5537. *
  5538. * @param stdClass $forum The forum
  5539. * @return bool
  5540. */
  5541. function forum_is_cutoff_date_reached($forum) {
  5542. $entityfactory = \mod_forum\local\container::get_entity_factory();
  5543. $coursemoduleinfo = get_fast_modinfo($forum->course);
  5544. $cminfo = $coursemoduleinfo->instances['forum'][$forum->id];
  5545. $forumentity = $entityfactory->get_forum_from_stdclass(
  5546. $forum,
  5547. context_module::instance($cminfo->id),
  5548. $cminfo->get_course_module_record(),
  5549. $cminfo->get_course()
  5550. );
  5551. return $forumentity->is_cutoff_date_reached();
  5552. }
  5553. /**
  5554. * Determine whether the specified forum's due date is reached.
  5555. *
  5556. * @param stdClass $forum The forum
  5557. * @return bool
  5558. */
  5559. function forum_is_due_date_reached($forum) {
  5560. $entityfactory = \mod_forum\local\container::get_entity_factory();
  5561. $coursemoduleinfo = get_fast_modinfo($forum->course);
  5562. $cminfo = $coursemoduleinfo->instances['forum'][$forum->id];
  5563. $forumentity = $entityfactory->get_forum_from_stdclass(
  5564. $forum,
  5565. context_module::instance($cminfo->id),
  5566. $cminfo->get_course_module_record(),
  5567. $cminfo->get_course()
  5568. );
  5569. return $forumentity->is_due_date_reached();
  5570. }
  5571. /**
  5572. * Determine whether the specified discussion is time-locked.
  5573. *
  5574. * @param stdClass $forum The forum that the discussion belongs to
  5575. * @param stdClass $discussion The discussion to test
  5576. * @return bool
  5577. */
  5578. function forum_discussion_is_locked($forum, $discussion) {
  5579. $entityfactory = \mod_forum\local\container::get_entity_factory();
  5580. $coursemoduleinfo = get_fast_modinfo($forum->course);
  5581. $cminfo = $coursemoduleinfo->instances['forum'][$forum->id];
  5582. $forumentity = $entityfactory->get_forum_from_stdclass(
  5583. $forum,
  5584. context_module::instance($cminfo->id),
  5585. $cminfo->get_course_module_record(),
  5586. $cminfo->get_course()
  5587. );
  5588. $discussionentity = $entityfactory->get_discussion_from_stdclass($discussion);
  5589. return $forumentity->is_discussion_locked($discussionentity);
  5590. }
  5591. /**
  5592. * Check if the module has any update that affects the current user since a given time.
  5593. *
  5594. * @param cm_info $cm course module data
  5595. * @param int $from the time to check updates from
  5596. * @param array $filter if we need to check only specific updates
  5597. * @return stdClass an object with the different type of areas indicating if they were updated or not
  5598. * @since Moodle 3.2
  5599. */
  5600. function forum_check_updates_since(cm_info $cm, $from, $filter = array()) {
  5601. $context = $cm->context;
  5602. $updates = new stdClass();
  5603. if (!has_capability('mod/forum:viewdiscussion', $context)) {
  5604. return $updates;
  5605. }
  5606. $updates = course_check_module_updates_since($cm, $from, array(), $filter);
  5607. // Check if there are new discussions in the forum.
  5608. $updates->discussions = (object) array('updated' => false);
  5609. $discussions = forum_get_discussions($cm, '', false, -1, -1, true, -1, 0, FORUM_POSTS_ALL_USER_GROUPS, $from);
  5610. if (!empty($discussions)) {
  5611. $updates->discussions->updated = true;
  5612. $updates->discussions->itemids = array_keys($discussions);
  5613. }
  5614. return $updates;
  5615. }
  5616. /**
  5617. * Check if the user can create attachments in a forum.
  5618. * @param stdClass $forum forum object
  5619. * @param stdClass $context context object
  5620. * @return bool true if the user can create attachments, false otherwise
  5621. * @since Moodle 3.3
  5622. */
  5623. function forum_can_create_attachment($forum, $context) {
  5624. // If maxbytes == 1 it means no attachments at all.
  5625. if (empty($forum->maxattachments) || $forum->maxbytes == 1 ||
  5626. !has_capability('mod/forum:createattachment', $context)) {
  5627. return false;
  5628. }
  5629. return true;
  5630. }
  5631. /**
  5632. * Get icon mapping for font-awesome.
  5633. *
  5634. * @return array
  5635. */
  5636. function mod_forum_get_fontawesome_icon_map() {
  5637. return [
  5638. 'mod_forum:i/pinned' => 'fa-map-pin',
  5639. 'mod_forum:t/selected' => 'fa-check',
  5640. 'mod_forum:t/subscribed' => 'fa-envelope-o',
  5641. 'mod_forum:t/unsubscribed' => 'fa-envelope-open-o',
  5642. 'mod_forum:t/star' => 'fa-star',
  5643. ];
  5644. }
  5645. /**
  5646. * Callback function that determines whether an action event should be showing its item count
  5647. * based on the event type and the item count.
  5648. *
  5649. * @param calendar_event $event The calendar event.
  5650. * @param int $itemcount The item count associated with the action event.
  5651. * @return bool
  5652. */
  5653. function mod_forum_core_calendar_event_action_shows_item_count(calendar_event $event, $itemcount = 0) {
  5654. // Always show item count for forums if item count is greater than 1.
  5655. // If only one action is required than it is obvious and we don't show it for other modules.
  5656. return $itemcount > 1;
  5657. }
  5658. /**
  5659. * This function receives a calendar event and returns the action associated with it, or null if there is none.
  5660. *
  5661. * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event
  5662. * is not displayed on the block.
  5663. *
  5664. * @param calendar_event $event
  5665. * @param \core_calendar\action_factory $factory
  5666. * @param int $userid User id to use for all capability checks, etc. Set to 0 for current user (default).
  5667. * @return \core_calendar\local\event\entities\action_interface|null
  5668. */
  5669. function mod_forum_core_calendar_provide_event_action(calendar_event $event,
  5670. \core_calendar\action_factory $factory,
  5671. int $userid = 0) {
  5672. global $DB, $USER;
  5673. if (!$userid) {
  5674. $userid = $USER->id;
  5675. }
  5676. $cm = get_fast_modinfo($event->courseid, $userid)->instances['forum'][$event->instance];
  5677. if (!$cm->uservisible) {
  5678. // The module is not visible to the user for any reason.
  5679. return null;
  5680. }
  5681. $context = context_module::instance($cm->id);
  5682. if (!has_capability('mod/forum:viewdiscussion', $context, $userid)) {
  5683. return null;
  5684. }
  5685. $completion = new \completion_info($cm->get_course());
  5686. $completiondata = $completion->get_data($cm, false, $userid);
  5687. if ($completiondata->completionstate != COMPLETION_INCOMPLETE) {
  5688. return null;
  5689. }
  5690. // Get action itemcount.
  5691. $itemcount = 0;
  5692. $forum = $DB->get_record('forum', array('id' => $cm->instance));
  5693. $postcountsql = "
  5694. SELECT
  5695. COUNT(1)
  5696. FROM
  5697. {forum_posts} fp
  5698. INNER JOIN {forum_discussions} fd ON fp.discussion=fd.id
  5699. WHERE
  5700. fp.userid=:userid AND fd.forum=:forumid";
  5701. $postcountparams = array('userid' => $userid, 'forumid' => $forum->id);
  5702. if ($forum->completiondiscussions) {
  5703. $count = $DB->count_records('forum_discussions', array('forum' => $forum->id, 'userid' => $userid));
  5704. $itemcount += ($forum->completiondiscussions >= $count) ? ($forum->completiondiscussions - $count) : 0;
  5705. }
  5706. if ($forum->completionreplies) {
  5707. $count = $DB->get_field_sql( $postcountsql.' AND fp.parent<>0', $postcountparams);
  5708. $itemcount += ($forum->completionreplies >= $count) ? ($forum->completionreplies - $count) : 0;
  5709. }
  5710. if ($forum->completionposts) {
  5711. $count = $DB->get_field_sql($postcountsql, $postcountparams);
  5712. $itemcount += ($forum->completionposts >= $count) ? ($forum->completionposts - $count) : 0;
  5713. }
  5714. // Well there is always atleast one actionable item (view forum, etc).
  5715. $itemcount = $itemcount > 0 ? $itemcount : 1;
  5716. return $factory->create_instance(
  5717. get_string('view'),
  5718. new \moodle_url('/mod/forum/view.php', ['id' => $cm->id]),
  5719. $itemcount,
  5720. true
  5721. );
  5722. }
  5723. /**
  5724. * Add a get_coursemodule_info function in case any forum type wants to add 'extra' information
  5725. * for the course (see resource).
  5726. *
  5727. * Given a course_module object, this function returns any "extra" information that may be needed
  5728. * when printing this activity in a course listing. See get_array_of_activities() in course/lib.php.
  5729. *
  5730. * @param stdClass $coursemodule The coursemodule object (record).
  5731. * @return cached_cm_info An object on information that the courses
  5732. * will know about (most noticeably, an icon).
  5733. */
  5734. function forum_get_coursemodule_info($coursemodule) {
  5735. global $DB;
  5736. $dbparams = ['id' => $coursemodule->instance];
  5737. $fields = 'id, name, intro, introformat, completionposts, completiondiscussions, completionreplies, duedate, cutoffdate';
  5738. if (!$forum = $DB->get_record('forum', $dbparams, $fields)) {
  5739. return false;
  5740. }
  5741. $result = new cached_cm_info();
  5742. $result->name = $forum->name;
  5743. if ($coursemodule->showdescription) {
  5744. // Convert intro to html. Do not filter cached version, filters run at display time.
  5745. $result->content = format_module_intro('forum', $forum, $coursemodule->id, false);
  5746. }
  5747. // Populate the custom completion rules as key => value pairs, but only if the completion mode is 'automatic'.
  5748. if ($coursemodule->completion == COMPLETION_TRACKING_AUTOMATIC) {
  5749. $result->customdata['customcompletionrules']['completiondiscussions'] = $forum->completiondiscussions;
  5750. $result->customdata['customcompletionrules']['completionreplies'] = $forum->completionreplies;
  5751. $result->customdata['customcompletionrules']['completionposts'] = $forum->completionposts;
  5752. }
  5753. // Populate some other values that can be used in calendar or on dashboard.
  5754. if ($forum->duedate) {
  5755. $result->customdata['duedate'] = $forum->duedate;
  5756. }
  5757. if ($forum->cutoffdate) {
  5758. $result->customdata['cutoffdate'] = $forum->cutoffdate;
  5759. }
  5760. return $result;
  5761. }
  5762. /**
  5763. * Callback which returns human-readable strings describing the active completion custom rules for the module instance.
  5764. *
  5765. * @param cm_info|stdClass $cm object with fields ->completion and ->customdata['customcompletionrules']
  5766. * @return array $descriptions the array of descriptions for the custom rules.
  5767. */
  5768. function mod_forum_get_completion_active_rule_descriptions($cm) {
  5769. // Values will be present in cm_info, and we assume these are up to date.
  5770. if (empty($cm->customdata['customcompletionrules'])
  5771. || $cm->completion != COMPLETION_TRACKING_AUTOMATIC) {
  5772. return [];
  5773. }
  5774. $descriptions = [];
  5775. foreach ($cm->customdata['customcompletionrules'] as $key => $val) {
  5776. switch ($key) {
  5777. case 'completiondiscussions':
  5778. if (!empty($val)) {
  5779. $descriptions[] = get_string('completiondiscussionsdesc', 'forum', $val);
  5780. }
  5781. break;
  5782. case 'completionreplies':
  5783. if (!empty($val)) {
  5784. $descriptions[] = get_string('completionrepliesdesc', 'forum', $val);
  5785. }
  5786. break;
  5787. case 'completionposts':
  5788. if (!empty($val)) {
  5789. $descriptions[] = get_string('completionpostsdesc', 'forum', $val);
  5790. }
  5791. break;
  5792. default:
  5793. break;
  5794. }
  5795. }
  5796. return $descriptions;
  5797. }
  5798. /**
  5799. * Check whether the forum post is a private reply visible to this user.
  5800. *
  5801. * @param stdClass $post The post to check.
  5802. * @param cm_info $cm The context module instance.
  5803. * @return bool Whether the post is visible in terms of private reply configuration.
  5804. */
  5805. function forum_post_is_visible_privately($post, $cm) {
  5806. global $USER;
  5807. if (!empty($post->privatereplyto)) {
  5808. // Allow the user to see the private reply if:
  5809. // * they hold the permission;
  5810. // * they are the author; or
  5811. // * they are the intended recipient.
  5812. $cansee = false;
  5813. $cansee = $cansee || ($post->userid == $USER->id);
  5814. $cansee = $cansee || ($post->privatereplyto == $USER->id);
  5815. $cansee = $cansee || has_capability('mod/forum:readprivatereplies', context_module::instance($cm->id));
  5816. return $cansee;
  5817. }
  5818. return true;
  5819. }
  5820. /**
  5821. * Check whether the user can reply privately to the parent post.
  5822. *
  5823. * @param \context_module $context
  5824. * @param \stdClass $parent
  5825. * @return bool
  5826. */
  5827. function forum_user_can_reply_privately(\context_module $context, \stdClass $parent) : bool {
  5828. if ($parent->privatereplyto) {
  5829. // You cannot reply privately to a post which is, itself, a private reply.
  5830. return false;
  5831. }
  5832. return has_capability('mod/forum:postprivatereply', $context);
  5833. }
  5834. /**
  5835. * This function calculates the minimum and maximum cutoff values for the timestart of
  5836. * the given event.
  5837. *
  5838. * It will return an array with two values, the first being the minimum cutoff value and
  5839. * the second being the maximum cutoff value. Either or both values can be null, which
  5840. * indicates there is no minimum or maximum, respectively.
  5841. *
  5842. * If a cutoff is required then the function must return an array containing the cutoff
  5843. * timestamp and error string to display to the user if the cutoff value is violated.
  5844. *
  5845. * A minimum and maximum cutoff return value will look like:
  5846. * [
  5847. * [1505704373, 'The date must be after this date'],
  5848. * [1506741172, 'The date must be before this date']
  5849. * ]
  5850. *
  5851. * @param calendar_event $event The calendar event to get the time range for
  5852. * @param stdClass $forum The module instance to get the range from
  5853. * @return array Returns an array with min and max date.
  5854. */
  5855. function mod_forum_core_calendar_get_valid_event_timestart_range(\calendar_event $event, \stdClass $forum) {
  5856. global $CFG;
  5857. require_once($CFG->dirroot . '/mod/forum/locallib.php');
  5858. $mindate = null;
  5859. $maxdate = null;
  5860. if ($event->eventtype == FORUM_EVENT_TYPE_DUE) {
  5861. if (!empty($forum->cutoffdate)) {
  5862. $maxdate = [
  5863. $forum->cutoffdate,
  5864. get_string('cutoffdatevalidation', 'forum'),
  5865. ];
  5866. }
  5867. }
  5868. return [$mindate, $maxdate];
  5869. }
  5870. /**
  5871. * This function will update the forum module according to the
  5872. * event that has been modified.
  5873. *
  5874. * It will set the timeclose value of the forum instance
  5875. * according to the type of event provided.
  5876. *
  5877. * @throws \moodle_exception
  5878. * @param \calendar_event $event
  5879. * @param stdClass $forum The module instance to get the range from
  5880. */
  5881. function mod_forum_core_calendar_event_timestart_updated(\calendar_event $event, \stdClass $forum) {
  5882. global $CFG, $DB;
  5883. require_once($CFG->dirroot . '/mod/forum/locallib.php');
  5884. if ($event->eventtype != FORUM_EVENT_TYPE_DUE) {
  5885. return;
  5886. }
  5887. $courseid = $event->courseid;
  5888. $modulename = $event->modulename;
  5889. $instanceid = $event->instance;
  5890. // Something weird going on. The event is for a different module so
  5891. // we should ignore it.
  5892. if ($modulename != 'forum') {
  5893. return;
  5894. }
  5895. if ($forum->id != $instanceid) {
  5896. return;
  5897. }
  5898. $coursemodule = get_fast_modinfo($courseid)->instances[$modulename][$instanceid];
  5899. $context = context_module::instance($coursemodule->id);
  5900. // The user does not have the capability to modify this activity.
  5901. if (!has_capability('moodle/course:manageactivities', $context)) {
  5902. return;
  5903. }
  5904. if ($event->eventtype == FORUM_EVENT_TYPE_DUE) {
  5905. if ($forum->duedate != $event->timestart) {
  5906. $forum->duedate = $event->timestart;
  5907. $forum->timemodified = time();
  5908. // Persist the instance changes.
  5909. $DB->update_record('forum', $forum);
  5910. $event = \core\event\course_module_updated::create_from_cm($coursemodule, $context);
  5911. $event->trigger();
  5912. }
  5913. }
  5914. }
  5915. /**
  5916. * Fetch the data used to display the discussions on the current page.
  5917. *
  5918. * @param \mod_forum\local\entities\forum $forum The forum entity
  5919. * @param stdClass $user The user to render for
  5920. * @param int[]|null $groupid The group to render
  5921. * @param int|null $sortorder The sort order to use when selecting the discussions in the list
  5922. * @param int|null $pageno The zero-indexed page number to use
  5923. * @param int|null $pagesize The number of discussions to show on the page
  5924. * @return array The data to use for display
  5925. */
  5926. function mod_forum_get_discussion_summaries(\mod_forum\local\entities\forum $forum, stdClass $user, ?int $groupid, ?int $sortorder,
  5927. ?int $pageno = 0, ?int $pagesize = 0) {
  5928. $vaultfactory = mod_forum\local\container::get_vault_factory();
  5929. $discussionvault = $vaultfactory->get_discussions_in_forum_vault();
  5930. $managerfactory = mod_forum\local\container::get_manager_factory();
  5931. $capabilitymanager = $managerfactory->get_capability_manager($forum);
  5932. $groupids = mod_forum_get_groups_from_groupid($forum, $user, $groupid);
  5933. if (null === $groupids) {
  5934. return $discussions = $discussionvault->get_from_forum_id(
  5935. $forum->get_id(),
  5936. $capabilitymanager->can_view_hidden_posts($user),
  5937. $user->id,
  5938. $sortorder,
  5939. $pagesize,
  5940. $pageno * $pagesize);
  5941. } else {
  5942. return $discussions = $discussionvault->get_from_forum_id_and_group_id(
  5943. $forum->get_id(),
  5944. $groupids,
  5945. $capabilitymanager->can_view_hidden_posts($user),
  5946. $user->id,
  5947. $sortorder,
  5948. $pagesize,
  5949. $pageno * $pagesize);
  5950. }
  5951. }
  5952. /**
  5953. * Get a count of all discussions in a forum.
  5954. *
  5955. * @param \mod_forum\local\entities\forum $forum The forum entity
  5956. * @param stdClass $user The user to render for
  5957. * @param int $groupid The group to render
  5958. * @return int The number of discussions in a forum
  5959. */
  5960. function mod_forum_count_all_discussions(\mod_forum\local\entities\forum $forum, stdClass $user, ?int $groupid) {
  5961. $managerfactory = mod_forum\local\container::get_manager_factory();
  5962. $capabilitymanager = $managerfactory->get_capability_manager($forum);
  5963. $vaultfactory = mod_forum\local\container::get_vault_factory();
  5964. $discussionvault = $vaultfactory->get_discussions_in_forum_vault();
  5965. $groupids = mod_forum_get_groups_from_groupid($forum, $user, $groupid);
  5966. if (null === $groupids) {
  5967. return $discussionvault->get_total_discussion_count_from_forum_id(
  5968. $forum->get_id(),
  5969. $capabilitymanager->can_view_hidden_posts($user),
  5970. $user->id);
  5971. } else {
  5972. return $discussionvault->get_total_discussion_count_from_forum_id_and_group_id(
  5973. $forum->get_id(),
  5974. $groupids,
  5975. $capabilitymanager->can_view_hidden_posts($user),
  5976. $user->id);
  5977. }
  5978. }
  5979. /**
  5980. * Get the list of groups to show based on the current user and requested groupid.
  5981. *
  5982. * @param \mod_forum\local\entities\forum $forum The forum entity
  5983. * @param stdClass $user The user viewing
  5984. * @param int $groupid The groupid requested
  5985. * @return array The list of groups to show
  5986. */
  5987. function mod_forum_get_groups_from_groupid(\mod_forum\local\entities\forum $forum, stdClass $user, ?int $groupid) : ?array {
  5988. $effectivegroupmode = $forum->get_effective_group_mode();
  5989. if (empty($effectivegroupmode)) {
  5990. // This forum is not in a group mode. Show all posts always.
  5991. return null;
  5992. }
  5993. if (null == $groupid) {
  5994. $managerfactory = mod_forum\local\container::get_manager_factory();
  5995. $capabilitymanager = $managerfactory->get_capability_manager($forum);
  5996. // No group was specified.
  5997. $showallgroups = (VISIBLEGROUPS == $effectivegroupmode);
  5998. $showallgroups = $showallgroups || $capabilitymanager->can_access_all_groups($user);
  5999. if ($showallgroups) {
  6000. // Return null to show all groups.
  6001. return null;
  6002. } else {
  6003. // No group was specified. Only show the users current groups.
  6004. return array_keys(
  6005. groups_get_all_groups(
  6006. $forum->get_course_id(),
  6007. $user->id,
  6008. $forum->get_course_module_record()->groupingid
  6009. )
  6010. );
  6011. }
  6012. } else {
  6013. // A group was specified. Just show that group.
  6014. return [$groupid];
  6015. }
  6016. }
  6017. /**
  6018. * Return a list of all the user preferences used by mod_forum.
  6019. *
  6020. * @return array
  6021. */
  6022. function mod_forum_user_preferences() {
  6023. $vaultfactory = \mod_forum\local\container::get_vault_factory();
  6024. $discussionlistvault = $vaultfactory->get_discussions_in_forum_vault();
  6025. $preferences = array();
  6026. $preferences['forum_discussionlistsortorder'] = array(
  6027. 'null' => NULL_NOT_ALLOWED,
  6028. 'default' => $discussionlistvault::SORTORDER_LASTPOST_DESC,
  6029. 'type' => PARAM_INT,
  6030. 'choices' => array(
  6031. $discussionlistvault::SORTORDER_LASTPOST_DESC,
  6032. $discussionlistvault::SORTORDER_LASTPOST_ASC,
  6033. $discussionlistvault::SORTORDER_CREATED_DESC,
  6034. $discussionlistvault::SORTORDER_CREATED_ASC,
  6035. $discussionlistvault::SORTORDER_REPLIES_DESC,
  6036. $discussionlistvault::SORTORDER_REPLIES_ASC
  6037. )
  6038. );
  6039. $preferences['forum_useexperimentalui'] = [
  6040. 'null' => NULL_NOT_ALLOWED,
  6041. 'default' => false,
  6042. 'type' => PARAM_BOOL
  6043. ];
  6044. return $preferences;
  6045. }
  6046. /**
  6047. * Lists all gradable areas for the advanced grading methods gramework.
  6048. *
  6049. * @return array('string'=>'string') An array with area names as keys and descriptions as values
  6050. */
  6051. function forum_grading_areas_list() {
  6052. return [
  6053. 'forum' => get_string('grade_forum_header', 'forum'),
  6054. ];
  6055. }
  6056. /**
  6057. * Callback to fetch the activity event type lang string.
  6058. *
  6059. * @param string $eventtype The event type.
  6060. * @return lang_string The event type lang string.
  6061. */
  6062. function mod_forum_core_calendar_get_event_action_string(string $eventtype): string {
  6063. global $CFG;
  6064. require_once($CFG->dirroot . '/mod/forum/locallib.php');
  6065. $modulename = get_string('modulename', 'forum');
  6066. if ($eventtype == FORUM_EVENT_TYPE_DUE) {
  6067. return get_string('calendardue', 'forum', $modulename);
  6068. } else {
  6069. return get_string('requiresaction', 'calendar', $modulename);
  6070. }
  6071. }