PageRenderTime 86ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 1ms

/mod_forumng_post.php

https://github.com/NigelCunningham/moodle-mod_forumng
PHP | 2334 lines | 1452 code | 257 blank | 625 comment | 280 complexity | 35cf0632da6f2449617c95779da7ca4a MD5 | raw file

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. // This file is part of Moodle - http://moodle.org/
  3. //
  4. // Moodle is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // Moodle is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU General Public License
  15. // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
  16. /**
  17. * Represents a single forum post.
  18. * @see mod_forumng_discussion
  19. * @see forum
  20. * @package mod
  21. * @subpackage forumng
  22. * @copyright 2011 The Open University
  23. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24. */
  25. class mod_forumng_post {
  26. const PARENT_NOT_LOADED = 'not_loaded';
  27. const PARENTPOST_DEPTH_PER_QUERY = 8;
  28. // For option definitions, see forumngtype.php display_post function
  29. const OPTION_EMAIL = 'email';
  30. const OPTION_DIGEST = 'digest';
  31. const OPTION_COMMAND_REPLY = 'command_reply';
  32. const OPTION_COMMAND_EDIT = 'command_edit';
  33. const OPTION_COMMAND_DELETE = 'command_delete';
  34. const OPTION_COMMAND_UNDELETE = 'command_undelete';
  35. const OPTION_COMMAND_SPLIT = 'command_split';
  36. const OPTION_COMMAND_HISTORY = 'command_history';
  37. const OPTION_COMMAND_REPORT = 'command_report';
  38. const OPTION_COMMAND_DIRECTLINK = 'command_directlink';
  39. const OPTION_VIEW_FULL_NAMES = 'view_full_names';
  40. const OPTION_TIME_ZONE = 'time_zone';
  41. const OPTION_SUMMARY = 'summary';
  42. const OPTION_NO_COMMANDS = 'no_commands';
  43. const OPTION_RATINGS_VIEW = 'ratings_view';
  44. const OPTION_RATINGS_EDIT = 'ratings_edit';
  45. const OPTION_VIEW_DELETED_INFO = 'deleted_info';
  46. const OPTION_EXPANDED = 'short';
  47. const OPTION_FLAG_CONTROL = 'flag_control';
  48. const OPTION_READ_TIME = 'read_time';
  49. const OPTION_CHILDREN_EXPANDED = 'children_expanded';
  50. const OPTION_CHILDREN_COLLAPSED = 'children_collapsed';
  51. const OPTION_INCLUDE_LOCK = 'include_lock';
  52. const OPTION_EXPORT = 'export';
  53. const OPTION_FULL_ADDRESSES = 'full_addresses';
  54. const OPTION_DISCUSSION_SUBJECT = 'discussion_subject';
  55. const OPTION_SELECTABLE = 'selectable';
  56. const OPTION_VISIBLE_POST_NUMBERS = 'visible_post_numbers';
  57. const OPTION_USER_IMAGE = 'user_image';
  58. const OPTION_PRINTABLE_VERSION = 'printable_version';
  59. const OPTION_JUMP_NEXT = 'jump_next';
  60. const OPTION_JUMP_PREVIOUS = 'jump_previous';
  61. const OPTION_JUMP_PARENT = 'jump_parent';
  62. const OPTION_FIRST_UNREAD = 'first_unread';
  63. const OPTION_UNREAD_NOT_HIGHLIGHTED = 'unread_not_highlighted';
  64. const OPTION_SINGLE_POST = 'single_post';
  65. /** Constant indicating that post is not rated by user */
  66. const NO_RATING = 999;
  67. // Object variables and accessors
  68. /////////////////////////////////
  69. private $discussion, $parentpost, $postfields, $full, $children,
  70. $forceexpand, $nextunread, $previousunread;
  71. /** @return mod_forumng The forum that this post is in */
  72. public function get_forum() { return $this->discussion->get_forum(); }
  73. /** @return mod_forumng_post Parent post*/
  74. public function get_parent() {
  75. if ($this->parentpost==self::PARENT_NOT_LOADED) {
  76. throw new coding_exception('Parent post not loaded');
  77. }
  78. return $this->parentpost;
  79. }
  80. /** @return mod_forumng_discussion The discussion that this post is in */
  81. public function get_discussion() { return $this->discussion; }
  82. /** @return int ID of this post */
  83. public function get_id() {
  84. return $this->postfields->id;
  85. }
  86. /** @return string Subject or null if no change in subject */
  87. public function get_subject() {
  88. return $this->postfields->subject;
  89. }
  90. /** @return int Post number [within discussion] */
  91. public function get_number() {
  92. if (!property_exists($this->postfields, 'number')) {
  93. throw new coding_exception('Post number not available here');
  94. }
  95. return $this->postfields->number;
  96. }
  97. /**
  98. * Use to obtain link parameters when linking to any page that has anything
  99. * to do with posts.
  100. */
  101. public function get_link_params($type, $currentuser = false) {
  102. global $USER;
  103. $params = 'p=' . $this->postfields->id .
  104. $this->get_forum()->get_clone_param($type);
  105. if ($currentuser) {
  106. $author = $this->get_user();
  107. if ($author->id == $USER->id) {
  108. $params .= '&currentuser=1';
  109. }
  110. }
  111. return $params;
  112. }
  113. /**
  114. * @return bool True if can flag
  115. */
  116. function can_flag() {
  117. // Cannot flag for deleted post
  118. if ($this->get_deleted() || $this->discussion->is_deleted()) {
  119. return false;
  120. }
  121. // The guest user cannot flag
  122. if (isguestuser()) {
  123. return false;
  124. }
  125. return true;
  126. }
  127. /** @return bool True if post is flagged by current user */
  128. public function is_flagged() {
  129. if (!property_exists($this->postfields, 'flagged')) {
  130. throw new coding_exception('Flagged information not available here');
  131. }
  132. return $this->postfields->flagged ? true : false;
  133. }
  134. /**
  135. * @param bool $flag True to set flag
  136. * @param int $userid User ID or 0 for current
  137. */
  138. public function set_flagged($flag, $userid=0) {
  139. global $DB;
  140. $userid = mod_forumng_utils::get_real_userid($userid);
  141. if ($flag) {
  142. $transaction = $DB->start_delegated_transaction();
  143. // Check there is not already a row
  144. if (!$DB->record_exists('forumng_flags',
  145. array('postid' => $this->get_id(), 'userid' => $userid))) {
  146. // Insert new row
  147. $newflag = (object)array('postid' => $this->get_id(),
  148. 'userid' => $userid, 'flagged' => time());
  149. $DB->insert_record('forumng_flags', $newflag);
  150. }
  151. // Note: Under rare circumstances this could result in two rows
  152. // for the same post and user, resulting in duplicates being
  153. // returned. This is dealt with in mod_forumng::get_flagged_posts.
  154. $transaction->allow_commit();
  155. } else {
  156. $DB->delete_records('forumng_flags',
  157. array('postid' => $this->get_id(), 'userid' => $userid));
  158. }
  159. }
  160. /**
  161. * Obtains the subject to use for this post where a subject is required
  162. * (should not be blank), such as in email. May be of the form Re:
  163. * <parent subject>. This function call makes a database query if the full
  164. * discussion was not loaded into memory.
  165. * @param bool $expectingquery Set to true if you think this might make
  166. * a db query (to prevent the warning)
  167. * @return string Subject
  168. */
  169. public function get_effective_subject($expectingquery = false) {
  170. if (property_exists($this->postfields, 'effectivesubject')) {
  171. return $this->postfields->effectivesubject;
  172. }
  173. // If subject is set in this post, return it
  174. if (!is_null($this->postfields->subject)) {
  175. $this->postfields->effectivesubject = $this->postfields->subject;
  176. return $this->postfields->effectivesubject;
  177. }
  178. // See if we already have other posts loaded
  179. if ($this->parentpost == self::PARENT_NOT_LOADED) {
  180. // Posts are not loaded, do a database query
  181. if (!$expectingquery) {
  182. debugging('This get method made a DB query; if this is expected,
  183. set the flag to say so', DEBUG_DEVELOPER);
  184. }
  185. $this->postfields->effectivesubject = self::
  186. inner_get_recursive_subject($this->postfields->parentpostid);
  187. return $this->postfields->effectivesubject;
  188. } else {
  189. // Posts are loaded, loop through them to find subject
  190. for ($parent = $this->parentpost; $parent!=null;
  191. $parent = $parent->parentpost) {
  192. if ($parent->postfields->subject!==null) {
  193. return get_string('re', 'forumng',
  194. $parent->postfields->subject);
  195. }
  196. }
  197. return '[subject error]'; // shouldn't get here
  198. }
  199. }
  200. /**
  201. * Given a post id - or the id of some ancestor of a post - this query
  202. * obtains the next (up to) 8 ancestors and returns a 'Re:' subject line
  203. * corresponding to the first ancestor which has a subject. If none of
  204. * the 8 have a subject, it makes another query to retrieve the next 8,
  205. * and so on.
  206. * @param int $parentid ID of a child post that we are trying to find
  207. * the subject from a parent of
  208. * @return string Subject of post ('Re: something')
  209. */
  210. private static function inner_get_recursive_subject($parentid) {
  211. global $DB;
  212. // Although the query looks scary because it has so many left joins,
  213. // in testing it worked quickly. The db just does eight primary-key
  214. // lookups. Analysis of existing posts in our database showed that
  215. // doing 8 levels is currently sufficient for about 98.7% of posts.
  216. $select = '';
  217. $join = '';
  218. $maxdepth = self::PARENTPOST_DEPTH_PER_QUERY;
  219. for ($depth = 1; $depth <= $maxdepth; $depth++) {
  220. $select .= "p$depth.subject AS s$depth, p$depth.deleted AS d$depth, ";
  221. if ($depth >= 2) {
  222. $prev = $depth - 1;
  223. $join .= "LEFT JOIN {forumng_posts} p$depth
  224. ON p$depth.id = p$prev.parentpostid ";
  225. }
  226. }
  227. do {
  228. $rec = $DB->get_record_sql("
  229. SELECT
  230. $select
  231. p$maxdepth.parentpostid AS nextparent
  232. FROM
  233. {forumng_posts} p1
  234. $join
  235. WHERE
  236. p1.id = ?
  237. ", array($parentid), '*', MUST_EXIST);
  238. for ($depth = 1; $depth <= $maxdepth; $depth++) {
  239. $var = "s$depth";
  240. $var2 = "d$depth";
  241. if ($rec->{$var} !== null && $rec->{$var2}==0) {
  242. return get_string('re', 'forumng', $rec->{$var});
  243. }
  244. }
  245. $parentid = $rec->nextparent;
  246. } while ($parentid);
  247. // If the database and memory representations are correct, we shouldn't
  248. // really get here because the top-level post always has a subject
  249. return '';
  250. }
  251. /** @return object User who created this post */
  252. public function get_user() {
  253. if (!property_exists($this->postfields, 'user')) {
  254. throw new coding_exception('User is not available at this point.');
  255. }
  256. return $this->postfields->user;
  257. }
  258. /** @return object User who last edited this post or null if no edits */
  259. public function get_edit_user() {
  260. if (!property_exists($this->postfields, 'edituser')) {
  261. throw new coding_exception('Edit user is not available at this point.');
  262. }
  263. return is_null($this->postfields->edituserid)
  264. ? null : $this->postfields->edituser;
  265. }
  266. /** @return int Time post was originally created */
  267. public function get_created() { return $this->postfields->created; }
  268. /** @return int Time post was most recently modified */
  269. public function get_modified() { return $this->postfields->modified; }
  270. /** @return int 0 if post is not deleted, otherwise time of deletion */
  271. public function get_deleted() { return $this->postfields->deleted; }
  272. /** @return object User object (basic fields) of deleter */
  273. public function get_delete_user() { return $this->postfields->deleteuser; }
  274. /** @return bool True if this is an old version of a post */
  275. public function is_old_version() {
  276. return $this->postfields->oldversion ? true : false;
  277. }
  278. /** @return bool True if the post is important */
  279. public function is_important() {
  280. return $this->postfields->important ? true : false;
  281. }
  282. /** @return string Message data from database (May be in arbitrary format) */
  283. public function get_raw_message() {
  284. return $this->postfields->message;
  285. }
  286. /** @return int Format of message (Moodle FORMAT_xx constant) */
  287. public function get_format() {
  288. return $this->postfields->messageformat;
  289. }
  290. /**
  291. * @return string Message after format_text and replacing file URLs
  292. */
  293. public function get_formatted_message() {
  294. global $CFG;
  295. require_once($CFG->dirroot . '/lib/filelib.php');
  296. $text = $this->postfields->message;
  297. $forum = $this->get_forum();
  298. // Add clone param to end of pluginfile requests
  299. if ($forum->is_shared()) {
  300. // "@@PLUGINFILE@@/cheese.gif?download=1"
  301. $text = preg_replace('~([\'"]@@PLUGINFILE@@[^\'"?]+)\?~',
  302. '$1?clone=' . $forum->get_course_module_id() . '&amp;', $text);
  303. // "@@PLUGINFILE@@/cheese.gif"
  304. $text = preg_replace('~([\'"]@@PLUGINFILE@@[^\'"?]+)([\'"])~',
  305. '$1?clone=' . $forum->get_course_module_id() . '$2', $text);
  306. }
  307. $text = file_rewrite_pluginfile_urls($text, 'pluginfile.php',
  308. $forum->get_context(true)->id, 'mod_forumng', 'message',
  309. $this->postfields->id);
  310. $textoptions = new stdClass();
  311. // Don't put a <p> tag round post
  312. $textoptions->para = false;
  313. // Does not indicate that we trust the text, only that the
  314. // TRUSTTEXT marker would be supported. At present though it isn't (hm)
  315. $textoptions->trusttext = false;
  316. return format_text($text, $this->postfields->messageformat, $textoptions,
  317. $forum->get_course_id());
  318. }
  319. /**
  320. * @return string Message after format_text_email and replacing file URLs
  321. */
  322. public function get_email_message() {
  323. global $CFG;
  324. require_once($CFG->dirroot . '/lib/filelib.php');
  325. $text = file_rewrite_pluginfile_urls($this->postfields->message, 'pluginfile.php',
  326. $this->get_forum()->get_context(true)->id, 'mod_forumng', 'message',
  327. $this->postfields->id);
  328. return format_text_email($text, $this->postfields->messageformat);
  329. }
  330. /** @return bool True if this message has one or more attachments */
  331. public function has_attachments() {
  332. return $this->postfields->attachments ? true : false;
  333. }
  334. /**
  335. * Gets the names of all attachments (if any)
  336. * @return array Array of attachment names (may be empty). Names only,
  337. * not including path to attachment folder
  338. */
  339. public function get_attachment_names() {
  340. $result = array();
  341. if (!$this->has_attachments()) {
  342. return $result;
  343. }
  344. $filecontext = $this->get_forum()->get_context(true);
  345. $fs = get_file_storage();
  346. foreach ($fs->get_area_files($filecontext->id, 'mod_forumng', 'attachment',
  347. $this->get_id(), 'filename', false) as $file) {
  348. $result[] = $file->get_filename();
  349. }
  350. return $result;
  351. }
  352. /**
  353. * @param string $attachment Attachment name (will not be checked for existence)
  354. * @return moodle_url URL to attachment
  355. */
  356. public function get_attachment_url($attachment) {
  357. $filecontext = $this->get_forum()->get_context(true);
  358. $params = array();
  359. if ($this->get_forum()->is_shared()) {
  360. $params['clone'] = $this->get_forum()->get_course_module_id();
  361. }
  362. return new moodle_url('/pluginfile.php/' . $filecontext->id . '/mod_forumng/attachment/' .
  363. $this->get_id() . '/' . $attachment, $params);
  364. }
  365. /**
  366. * @return string URL of this discussion
  367. */
  368. public function get_url() {
  369. return $this->get_discussion()->get_url() . '#p' . $this->get_id();
  370. }
  371. /**
  372. * Checks unread status (only available when requested as part of whole
  373. * discussion).
  374. * @return bool True if this post is unread
  375. * @throws mod_forumng_exception If unread data is not available
  376. */
  377. public function is_unread() {
  378. // Your own posts are always read (note: technically you can request
  379. // unread data for another user - so we use the id for whom data was
  380. // requested, not $USER->id directly).
  381. $userid = $this->discussion->get_unread_data_user_id();
  382. if (($this->postfields->edituserid == $userid) ||
  383. (!$this->postfields->edituserid
  384. && $this->postfields->userid==$userid)) {
  385. return false;
  386. }
  387. // Posts past sell-by are always read
  388. $deadline = mod_forumng::get_read_tracking_deadline();
  389. if ($this->postfields->modified < $deadline) {
  390. return false;
  391. }
  392. // Compare date to discussion read data
  393. return $this->postfields->modified > $this->discussion->get_time_read();
  394. }
  395. /**
  396. * Checks unread status of child posts (only available when requested as
  397. * part of whole discussion). Not a recursive method - checks only one
  398. * level of children.
  399. * @return bool True if any of the children of this post are unread
  400. */
  401. public function has_unread_child() {
  402. $this->require_children();
  403. foreach ($this->children as $child) {
  404. if ($child->is_unread()) {
  405. return true;
  406. }
  407. }
  408. return false;
  409. }
  410. /**
  411. * Checks if this post has any children (replies).
  412. * @return bool True if post has one or more replies
  413. */
  414. public function has_children() {
  415. $this->require_children();
  416. return count($this->children) > 0;
  417. }
  418. /**
  419. * Marks this post as being expanded from the start.
  420. */
  421. public function force_expand() {
  422. $this->forceexpand = true;
  423. }
  424. /** @return bool True if this is the first post of a discussion */
  425. public function is_root_post() {
  426. return $this->postfields->parentpostid ? false : true;
  427. }
  428. /**
  429. * @throws mod_forumng_exception If rating information wasn't queried
  430. */
  431. private function check_ratings() {
  432. if (!property_exists($this->postfields, 'averagerating')) {
  433. throw new coding_exception('Rating information not retrieved');
  434. }
  435. }
  436. /**
  437. * @param bool $astext If true, returns a string rather than a number
  438. * @return mixed Average rating as float, or a string description if
  439. * $astext is true
  440. * @throws mod_forumng_exception If rating information wasn't queried
  441. */
  442. public function get_average_rating($astext = false) {
  443. $this->check_ratings();
  444. if ($astext) {
  445. $options = $this->get_forum()->get_rating_options();
  446. $value = (int)round($this->postfields->averagerating);
  447. if (array_key_exists($value, $options)) {
  448. return $options[$value];
  449. } else {
  450. return '?'; // Can occur if rating scale is changed
  451. }
  452. } else {
  453. return $this->postfields->averagerating;
  454. }
  455. }
  456. /**
  457. * @return int Number of ratings of this post (may be 0)
  458. */
  459. public function get_num_ratings() {
  460. $this->check_ratings();
  461. return $this->postfields->numratings;
  462. }
  463. /**
  464. * @return int Current user's rating of this post or null if none
  465. * @throws mod_forumng_exception If rating information wasn't queried
  466. */
  467. public function get_own_rating() {
  468. $this->check_ratings();
  469. return $this->postfields->ownrating;
  470. }
  471. /**
  472. * Obtains search document representing this post.
  473. * @return local_ousearch_document Document object
  474. */
  475. function search_get_document() {
  476. $doc = new local_ousearch_document();
  477. $doc->init_module_instance('forumng',
  478. $this->get_forum()->get_course_module(true));
  479. if ($groupid = $this->discussion->get_group_id()) {
  480. $doc->set_group_id($groupid);
  481. }
  482. $doc->set_int_refs($this->get_id());
  483. return $doc;
  484. }
  485. /**
  486. * @param array $out Array that receives list of this post and all
  487. * children (including nested children) in order
  488. */
  489. public function build_linear_children(&$out) {
  490. $this->require_children();
  491. $out[count($out)] = $this;
  492. foreach ($this->children as $child) {
  493. $child->build_linear_children($out);
  494. }
  495. }
  496. /**
  497. * Finds a child post (or this one) with the specified ID.
  498. * @param int $id Post ID
  499. * @param bool $toplevel True for initial request (makes it throw
  500. * exception if not found)
  501. * @return mod_forumng_post Child post
  502. */
  503. public function find_child($id, $toplevel=true) {
  504. if ($this->postfields->id == $id) {
  505. return $this;
  506. }
  507. $this->require_children();
  508. foreach ($this->children as $child) {
  509. $result = $child->find_child($id, false);
  510. if ($result) {
  511. return $result;
  512. }
  513. }
  514. if ($toplevel) {
  515. throw new coding_exception("Child id $id not found");
  516. }
  517. return null;
  518. }
  519. /**
  520. * Finds which child post (or this) has the most recent modified date.
  521. * @param mod_forumng_post &$newest Newest post (must be null when calling)
  522. */
  523. public function find_newest_child(&$newest) {
  524. if (!$newest || $newest->get_modified() < $this->get_modified()) {
  525. $newest = $this;
  526. }
  527. $this->require_children();
  528. foreach ($this->children as $child) {
  529. $child->find_newest_child($newest);
  530. }
  531. }
  532. /**
  533. * Adds the ID of all children (and this post itself) to a list.
  534. * @param array &$list List of IDs
  535. */
  536. public function list_child_ids(&$list) {
  537. $list[] = $this->get_id();
  538. $this->require_children();
  539. foreach ($this->children as $child) {
  540. $child->list_child_ids($list);
  541. }
  542. }
  543. /**
  544. * @return mod_forumng_post Next unread post or null if there are no more
  545. */
  546. public function get_next_unread() {
  547. $this->require_children();
  548. return $this->nextunread;
  549. }
  550. /**
  551. * @return mod_forumng_post Previous unread post or null if there are no more
  552. */
  553. public function get_previous_unread() {
  554. $this->require_children();
  555. return $this->previousunread;
  556. }
  557. /**
  558. * Used by discussion to set up the unread posts.
  559. * @param mod_forumng_post $nextunread
  560. * @param mod_forumng_post $previousunread
  561. */
  562. function set_unread_list($nextunread, $previousunread) {
  563. $this->nextunread = $nextunread;
  564. $this->previousunread = $previousunread;
  565. }
  566. // Factory method
  567. /////////////////
  568. /**
  569. * Creates a forum post object, forum object, and all related data from a
  570. * single forum post ID. Intended when entering a page which uses post ID
  571. * as a parameter.
  572. * @param int $id ID of forum post
  573. * @param int $cloneid If this is in a shared forum, must be the id of the
  574. * clone forum currently in use, or CLONE_DIRECT; otherwise must be 0
  575. * @param bool $wholediscussion If true, retrieves entire discussion
  576. * instead of just this single post
  577. * @param bool $usecache True to use cache when retrieving the discussion
  578. * @param int $userid User ID to get post on behalf of (controls flag data
  579. * retrieved)
  580. * @return mod_forumng_post Post object
  581. */
  582. public static function get_from_id($id, $cloneid,
  583. $wholediscussion=false, $usecache=false, $userid=0) {
  584. if ($wholediscussion) {
  585. $discussion = mod_forumng_discussion::get_from_post_id($id, $cloneid,
  586. $usecache, $usecache);
  587. $root = $discussion->get_root_post();
  588. return $root->find_child($id);
  589. } else {
  590. // Get post data (including extra data such as ratings and flags)
  591. $records = self::query_posts('fp.id = ?', array($id), 'fp.id', true,
  592. true, false, $userid);
  593. if (count($records)!=1) {
  594. throw new coding_exception("Invalid post ID $id");
  595. }
  596. $postfields = reset($records);
  597. $discussion = mod_forumng_discussion::get_from_id($postfields->discussionid, $cloneid);
  598. $newpost = new mod_forumng_post($discussion, $postfields);
  599. return $newpost;
  600. }
  601. }
  602. /**
  603. * Obtains a search document given the ousearch parameters.
  604. * @param object $document Object containing fields from the ousearch documents table
  605. * @return mixed False if object can't be found, otherwise object containing the following
  606. * fields: ->content, ->title, ->url, ->activityname, ->activityurl,
  607. * and optionally ->extrastrings array, ->data, ->hide
  608. */
  609. static function search_get_page($document) {
  610. global $DB, $CFG, $USER;
  611. // Implemented directly in SQL for performance, rather than using the
  612. // objects themselves
  613. $result = $DB->get_record_sql("
  614. SELECT
  615. fp.message AS content, fp.subject, firstpost.subject AS firstpostsubject,
  616. firstpost.id AS firstpostid, fd.id AS discussionid,
  617. f.name AS activityname, cm.id AS cmid, fd.timestart, fd.timeend,
  618. f.shared AS shared, f.type AS forumtype
  619. FROM
  620. {forumng_posts} fp
  621. INNER JOIN {forumng_discussions} fd ON fd.id = fp.discussionid
  622. INNER JOIN {forumng_posts} firstpost ON fd.postid = firstpost.id
  623. INNER JOIN {forumng} f ON fd.forumngid = f.id
  624. INNER JOIN {course_modules} cm ON cm.instance = f.id AND cm.course = f.course
  625. INNER JOIN {modules} m ON cm.module = m.id
  626. WHERE
  627. fp.id = ? AND m.name = 'forumng'", array($document->intref1), '*', IGNORE_MISSING);
  628. if (!$result) {
  629. return false;
  630. }
  631. // Title is either the post subject or Re: plus the discussion subject
  632. // if the post subject is blank
  633. $result->title = $result->subject;
  634. if (is_null($result->title)) {
  635. $result->title = get_string('re', 'forumng', $result->firstpostsubject);
  636. }
  637. // Link is to value in url if present, otherwise to original forum
  638. $cloneparam = $result->cmid;
  639. if ($result->shared) {
  640. global $FORUMNG_CLONE_MAP;
  641. if (!empty($FORUMNG_CLONE_MAP)) {
  642. $cloneparam = $FORUMNG_CLONE_MAP[$result->cmid]->id;
  643. $clonebit = '&amp;clone=' . $cloneparam;
  644. } else {
  645. $clonebit = '&amp;clone=' .
  646. ($cloneparam = optional_param('clone', $result->cmid, PARAM_INT));
  647. }
  648. } else {
  649. $clonebit = '';
  650. }
  651. // Work out URL to post
  652. $result->url = $CFG->wwwroot . '/mod/forumng/discuss.php?d=' .
  653. $result->discussionid . $clonebit . '#p' . $document->intref1;
  654. // Activity URL
  655. $result->activityurl = $CFG->wwwroot . '/mod/forumng/view.php?id=' .
  656. $result->cmid . $clonebit;
  657. // Hide results outside their time range (unless current user can see)
  658. $now = time();
  659. if ($now < $result->timestart || ($result->timeend && $now>=$result->timeend) &&
  660. !has_capability('mod/forumng:viewallposts',
  661. context_module::instance($result->cmid))) {
  662. $result->hide = true;
  663. }
  664. // Handle annoying forum types that hide discussions
  665. $type = forumngtype::get_new($result->forumtype);
  666. if ($type->has_unread_restriction()) {
  667. // TODO The name of the _unread_restriction should be _discussion_restriction.
  668. // This is going to be slow, we need to load the discussion
  669. $discussion = mod_forumng_discussion::get_from_id($result->discussionid, $cloneparam);
  670. if (!$type->can_view_discussion($discussion, $USER->id)) {
  671. $result->hide = true;
  672. }
  673. }
  674. return $result;
  675. }
  676. // Object methods
  677. /////////////////
  678. /**
  679. * @param mod_forumng_discussion $discussion Discussion object
  680. * @param object $postfields Post fields from DB table (may also include
  681. * some extra fields provided by mod_forumng_post::query_posts)
  682. * @param mod_forumng_post $parentpost Parent post or null if this is root post,
  683. * or PARENT_NOT_LOADED if not available
  684. */
  685. function __construct($discussion, $postfields, $parentpost=self::PARENT_NOT_LOADED) {
  686. $this->discussion = $discussion;
  687. $this->postfields = $postfields;
  688. // Extract the user details into Moodle user-like objects
  689. if (property_exists($postfields, 'u_id')) {
  690. $postfields->user = mod_forumng_utils::extract_subobject($postfields, 'u_');
  691. $postfields->edituser = mod_forumng_utils::extract_subobject($postfields, 'eu_');
  692. $postfields->deleteuser = mod_forumng_utils::extract_subobject($postfields, 'du_');
  693. }
  694. $this->parentpost = $parentpost;
  695. $this->children = false;
  696. }
  697. /**
  698. * Used to inform the post that all its children will be supplied.
  699. * Call before calling add_child(), or even if there are no children.
  700. */
  701. function init_children() {
  702. $this->children = array();
  703. }
  704. /**
  705. * For internal use only. Adds a child to this post while constructing
  706. * the tree of posts
  707. * @param mod_forumng_post $child Child post
  708. */
  709. function add_child($child) {
  710. $this->require_children();
  711. $this->children[] = $child;
  712. }
  713. /**
  714. * Checks that children are available.
  715. * @throws mod_forumng_exception If children have not been loaded
  716. */
  717. function require_children() {
  718. if (!is_array($this->children)) {
  719. throw new coding_exception('Requires child post data');
  720. }
  721. }
  722. /**
  723. * Internal function. Queries for posts.
  724. * @param string $where Where clause (fp is alias for post table)
  725. * @param array $whereparams Parameters (values for ? parameters) in where clause
  726. * @param string $order Sort order; the default is fp.id - note this is preferable
  727. * to fp.timecreated because it works correctly if there are two posts in
  728. * the same second
  729. * @param bool $ratings True if ratings should be included in the query
  730. * @param bool $flags True if flags should be included in the query
  731. * @param bool $effectivesubjects True if the query should include the
  732. * (complicated!) logic to obtain the 'effective subject'. This may result
  733. * in additional queries afterward for posts which are very deeply nested.
  734. * @param int $userid 0 = current user (at present this is only used for
  735. * flags)
  736. * @return array Resulting posts as array of Moodle records, empty array
  737. * if none
  738. */
  739. static function query_posts($where, $whereparams, $order='fp.id', $ratings=true,
  740. $flags=false, $effectivesubjects=false,
  741. $userid=0, $joindiscussion=false, $discussionsubject=false, $limitfrom='',
  742. $limitnum='') {
  743. global $DB, $USER;
  744. $userid = mod_forumng_utils::get_real_userid($userid);
  745. $queryparams = array();
  746. // We include ratings if these are enabled, otherwise save the database
  747. // some effort and don't bother
  748. if ($ratings) {
  749. $ratingsquery = ",
  750. (SELECT AVG(rating) FROM {forumng_ratings}
  751. WHERE postid = fp.id) AS averagerating,
  752. (SELECT COUNT(1) FROM {forumng_ratings}
  753. WHERE postid = fp.id) AS numratings,
  754. (SELECT rating FROM {forumng_ratings}
  755. WHERE postid = fp.id AND userid = ?) AS ownrating";
  756. // Add parameter to start of params list
  757. $queryparams[] = $USER->id;
  758. } else {
  759. $ratingsquery = '';
  760. }
  761. if ($flags) {
  762. $flagsjoin = "
  763. LEFT JOIN {forumng_flags} ff ON ff.postid = fp.id AND ff.userid = ?";
  764. $flagsquery = ", ff.flagged";
  765. $queryparams[] = $userid;
  766. } else {
  767. $flagsjoin = '';
  768. $flagsquery = '';
  769. }
  770. if ($joindiscussion) {
  771. $discussionjoin = "
  772. INNER JOIN {forumng_discussions} fd ON fp.discussionid = fd.id";
  773. $discussionquery = ',' . mod_forumng_utils::select_discussion_fields('fd');
  774. if ($discussionsubject) {
  775. $discussionjoin .= "
  776. INNER JOIN {forumng_posts} fdfp ON fd.postid = fdfp.id";
  777. $discussionquery .= ', fdfp.subject AS fd_subject';
  778. }
  779. } else {
  780. $discussionjoin = '';
  781. $discussionquery = '';
  782. }
  783. if ($effectivesubjects) {
  784. $maxdepth = self::PARENTPOST_DEPTH_PER_QUERY;
  785. $subjectsjoin = '';
  786. $subjectsquery = ", p$maxdepth.parentpostid AS nextparent ";
  787. for ($depth = 2; $depth <= $maxdepth; $depth++) {
  788. $subjectsquery .= ", p$depth.subject AS s$depth, p$depth.deleted AS d$depth";
  789. $prev = 'p'. ($depth - 1);
  790. if ($prev == 'p1') {
  791. $prev = 'fp';
  792. }
  793. $subjectsjoin .= "LEFT JOIN {forumng_posts} p$depth
  794. ON p$depth.id = $prev.parentpostid ";
  795. }
  796. } else {
  797. $subjectsjoin = '';
  798. $subjectsquery = '';
  799. }
  800. // Retrieve posts from discussion with incorporated user information
  801. // and ratings info if specified
  802. $results = $DB->get_records_sql("
  803. SELECT
  804. fp.*,
  805. ".mod_forumng_utils::select_username_fields('u').",
  806. ".mod_forumng_utils::select_username_fields('eu').",
  807. ".mod_forumng_utils::select_username_fields('du')."
  808. $ratingsquery
  809. $flagsquery
  810. $subjectsquery
  811. $discussionquery
  812. FROM
  813. {forumng_posts} fp
  814. INNER JOIN {user} u ON fp.userid = u.id
  815. LEFT JOIN {user} eu ON fp.edituserid = eu.id
  816. LEFT JOIN {user} du ON fp.deleteuserid = du.id
  817. $discussionjoin
  818. $flagsjoin
  819. $subjectsjoin
  820. WHERE
  821. $where
  822. ORDER BY
  823. $order
  824. ", array_merge($queryparams, $whereparams), $limitfrom, $limitnum);
  825. if ($effectivesubjects) {
  826. // Figure out the effective subject for each result
  827. foreach ($results as $result) {
  828. $got = false;
  829. if ($result->subject !== null) {
  830. $result->effectivesubject = $result->subject;
  831. $got = true;
  832. continue;
  833. }
  834. for ($depth = 2; $depth <= $maxdepth; $depth++) {
  835. $var = "s$depth";
  836. $var2 = "d$depth";
  837. if (!$got && $result->{$var} !== null && $result->{$var2}==0) {
  838. $result->effectivesubject = get_string('re', 'forumng', $result->{$var});
  839. $got = true;
  840. }
  841. unset($result->{$var});
  842. unset($result->{$var2});
  843. }
  844. if (!$got) {
  845. // Do extra queries to pick up subjects for posts where it
  846. // was unknown within the default depth. We can use the
  847. // 'nextparent' to get the ID of the parent post of the last
  848. // one that we checked already
  849. $result->effectivesubject = self::inner_get_recursive_subject(
  850. $result->nextparent);
  851. }
  852. }
  853. }
  854. return $results;
  855. }
  856. /**
  857. * Replies to the post
  858. * @param string $subject Subject
  859. * @param string $message Message
  860. * @param int $messageformat Moodle format used for message
  861. * @param bool $attachments True if post contains attachments
  862. * @param bool $setimportant If true, highlight the post
  863. * @param bool $mailnow If true, sends mail ASAP
  864. * @param int $userid User ID (0 = current)
  865. * @param bool $log True to log this reply
  866. * @return int ID of newly-created post
  867. */
  868. function reply($subject, $message, $messageformat,
  869. $attachments=false, $setimportant=false, $mailnow=false, $userid=0, $log=true) {
  870. global $DB;
  871. $transaction = $DB->start_delegated_transaction();
  872. $id = $this->discussion->create_reply($this, $subject, $message, $messageformat,
  873. $attachments, $setimportant, $mailnow, $userid);
  874. if ($log) {
  875. $this->log('add reply', $id);
  876. }
  877. $transaction->allow_commit();
  878. $this->get_discussion()->uncache();
  879. return $id;
  880. }
  881. /**
  882. * Updates the message field of a post entry. This is necessary in some cases where
  883. * the user includes images etc. in the message; these are initially included using
  884. * a draft URL which has to be changed to a special relative path on convert, and we
  885. * can't do that until the post ID is known. Additionally, we don't have a post object
  886. * at that point, hence use of static function.
  887. * @param int $postid ID of post to update
  888. * @param string $newtext Updated message text
  889. */
  890. static function update_message_for_files($postid, $newtext) {
  891. global $DB;
  892. $DB->set_field('forumng_posts', 'message', $newtext, array('id'=>$postid));
  893. }
  894. /**
  895. * Obtains a list of previous versions of this post (if any), in descending
  896. * order of modification date.
  897. * @return array Array of mod_forumng_post objects (empty if none)
  898. */
  899. function get_old_versions() {
  900. $postdata = self::query_posts(
  901. 'fp.oldversion = 1 AND fp.parentpostid = ?', array($this->postfields->id),
  902. 'fp.modified DESC', false, false);
  903. $posts = array();
  904. foreach ($postdata as $postfields) {
  905. $newpost = new mod_forumng_post($this->discussion, $postfields, $this);
  906. $posts[] = $newpost;
  907. }
  908. return $posts;
  909. }
  910. /**
  911. * Recursive function obtains all users IDs that made this post and all
  912. * child posts.
  913. * @param array &$userids Associative array from id=>true that receives
  914. * all user IDs
  915. */
  916. function list_all_user_ids(&$userids) {
  917. $this->require_children();
  918. // Add current ID
  919. $userid = $this->get_user()->id;
  920. if (!array_key_exists($userid, $userids)) {
  921. $userids[$userid] = true;
  922. }
  923. foreach ($this->children as $post) {
  924. $post->list_all_user_ids($userids);
  925. }
  926. }
  927. /**
  928. * NOTE: This method is the second stage of editing and must be called
  929. * after edit_start and after files are being updated. This is because
  930. * it depends on the result of file_save_draft_area_files.
  931. * @param string $message Message
  932. * @param int $messageformat Moodle format ID
  933. * @param bool $gotsubject True if message subject changed
  934. */
  935. function edit_finish($message, $messageformat, $gotsubject) {
  936. global $DB;
  937. $transaction = $DB->start_delegated_transaction();
  938. $update = new StdClass;
  939. if ($message!==$this->postfields->message) {
  940. $update->message = $message;
  941. }
  942. if ($messageformat != $this->postfields->messageformat) {
  943. $update->messageformat = $messageformat;
  944. }
  945. if (count((array)$update)>0) {
  946. $update->id = $this->postfields->id;
  947. $DB->update_record('forumng_posts', $update);
  948. }
  949. // Update in-memory representation
  950. foreach ((array)$update as $name=>$value) {
  951. $this->postfields->{$name} = $value;
  952. }
  953. // Uncache before updating search (want to make sure that the recursive
  954. // update gets latest data)
  955. $this->get_discussion()->uncache();
  956. // Update search index
  957. if ((isset($update->message) || $gotsubject)) {
  958. // Update for this post
  959. $this->search_update();
  960. // If changing the subject of a root post, update all posts in the
  961. // discussion (ugh)
  962. if ($this->is_root_post() && $gotsubject) {
  963. $this->search_update_children();
  964. }
  965. }
  966. $transaction->allow_commit();
  967. }
  968. /**
  969. * Edits an existing message. The previous version of the message is
  970. * retained for admins to view if needed.
  971. *
  972. * NOTE: This method is the first stage of editing and must be called
  973. * BEFORE files are updated. Ensure that there is a DB transaction around
  974. * the calls to these two methods.
  975. * @param string $subject Subject
  976. * @param bool $attachments True if post now contains attachments
  977. * @param bool $setimportant If true, highlight the post
  978. * @param bool $mailnow New value of mailnow flag (ignored if message was already mailed)
  979. * @param int $userid Userid doing the editing (0 = current)
  980. * @return bool True if subject changed (this is weird, but edit_finish
  981. * needs it)
  982. */
  983. function edit_start($subject, $attachments=false, $setimportant=false,
  984. $mailnow=false, $userid=0, $log=true) {
  985. global $DB;
  986. $now = time();
  987. // Create copy of existing entry ('old version')
  988. $copy = clone($this->postfields);
  989. // Copy has oldversion set to 1 and parentpost set to id of real post
  990. $copy->oldversion = 1;
  991. $copy->parentpostid = $copy->id;
  992. unset($copy->id);
  993. // OK, add copy
  994. $transaction = $DB->start_delegated_transaction();
  995. $copyid = $DB->insert_record('forumng_posts', $copy);
  996. // Move old attachments to copy (note: we will save new attachments from filemanager draft
  997. // area later)
  998. if ($this->has_attachments()) {
  999. $fs = get_file_storage();
  1000. $filecontext = $this->get_forum()->get_context(true);
  1001. foreach (array('attachment', 'message') as $filearea) {
  1002. $oldfiles = $fs->get_area_files($filecontext->id, 'mod_forumng', $filearea,
  1003. $this->get_id(), 'id', false);
  1004. foreach ($oldfiles as $oldfile) {
  1005. $filerecord = new stdClass();
  1006. $filerecord->itemid = $copyid;
  1007. $fs->create_file_from_storedfile($filerecord, $oldfile);
  1008. }
  1009. $fs->delete_area_files($filecontext->id, 'mod_forumng',
  1010. $filearea, $this->get_id());
  1011. }
  1012. }
  1013. // Update existing entry with new data where it changed
  1014. $update = new StdClass;
  1015. $gotsubject = false;
  1016. if ($subject!==$this->postfields->subject) {
  1017. $update->subject = strlen(trim($subject)) == 0 ? null : $subject;
  1018. $gotsubject = true;
  1019. }
  1020. if (!$attachments && $this->postfields->attachments) {
  1021. $update->attachments = 0;
  1022. } else if ($attachments && !$this->postfields->attachments) {
  1023. $update->attachments = 1;
  1024. }
  1025. if ($setimportant) {
  1026. $update->important = 1;
  1027. } else {
  1028. $update->important = 0;
  1029. }
  1030. if ($this->postfields->mailstate==mod_forumng::MAILSTATE_NOT_MAILED &&
  1031. $mailnow) {
  1032. $update->mailstate = mod_forumng::MAILSTATE_NOW_NOT_MAILED;
  1033. } else if ($this->postfields->mailstate==mod_forumng::MAILSTATE_NOW_NOT_MAILED &&
  1034. !$mailnow) {
  1035. $update->mailstate = mod_forumng::MAILSTATE_NOT_MAILED;
  1036. }
  1037. $update->modified = $now;
  1038. $update->edituserid = mod_forumng_utils::get_real_userid($userid);
  1039. $update->id = $this->postfields->id;
  1040. $DB->update_record('forumng_posts', $update);
  1041. if ($log) {
  1042. $this->log('edit post');
  1043. }
  1044. // Update in-memory representation
  1045. foreach ((array)$update as $name=>$value) {
  1046. $this->postfields->{$name} = $value;
  1047. }
  1048. // If this is the root post, then changing its subject affects
  1049. // the discussion subhject
  1050. if ($this->is_root_post() && $gotsubject) {
  1051. $this->discussion->hack_subject($this->postfields->subject);
  1052. }
  1053. $transaction->allow_commit();
  1054. return $gotsubject;
  1055. }
  1056. /**
  1057. * Updates search data for this post.
  1058. * @param bool $expectingquery True if it might need to make a query to
  1059. * get the subject
  1060. */
  1061. function search_update($expectingquery = false) {
  1062. if (!mod_forumng::search_installed()) {
  1063. return;
  1064. }
  1065. global $DB;
  1066. $searchdoc = $this->search_get_document();
  1067. $transaction = $DB->start_delegated_transaction();
  1068. if ($this->get_deleted() || $this->get_discussion()->is_deleted() ||
  1069. $this->get_discussion()->is_making_search_change()) {
  1070. if ($searchdoc->find()) {
  1071. $searchdoc->delete();
  1072. }
  1073. } else {
  1074. // $title here is not the title appearing in the search result
  1075. // but the text which decides the search score
  1076. $title = $this->get_subject();
  1077. $searchdoc->update($title, $this->get_formatted_message());
  1078. }
  1079. $transaction->allow_commit();
  1080. }
  1081. /**
  1082. * Calls search_update on each child of the current post, and recurses.
  1083. * Used when the subject's discussion is changed.
  1084. */
  1085. function search_update_children() {
  1086. if (!mod_forumng::search_installed()) {
  1087. return;
  1088. }
  1089. // If the in-memory post object isn't already part of a full
  1090. // discussion...
  1091. if (!is_array($this->children)) {
  1092. // ...then get one
  1093. $discussion = mod_forumng_discussion::get_from_id(
  1094. $this->discussion->get_id(),
  1095. $this->get_forum()->get_course_module_id());
  1096. $post = $discussion->get_root_post()->find_child($this->get_id());
  1097. // Do this update on the new discussion
  1098. $post->search_update_children();
  1099. return;
  1100. }
  1101. // Loop through all children
  1102. foreach ($this->children as $child) {
  1103. // Update its search fields
  1104. $child->search_update();
  1105. // Recurse
  1106. $child->search_update_children();
  1107. }
  1108. }
  1109. /**
  1110. * Marks a post as deleted.
  1111. * @param int $userid User ID to mark as having deleted the post
  1112. * @param bool $log If true, adds entry to Moodle log
  1113. */
  1114. function delete($userid=0, $log=true) {
  1115. global $DB;
  1116. if ($this->postfields->deleted) {
  1117. return;
  1118. }
  1119. if (!$this->postfields->parentpostid) {
  1120. throw new coding_exception('Cannot delete discussion root post');
  1121. }
  1122. $transaction = $DB->start_delegated_transaction();
  1123. // Mark this post as deleted
  1124. $update = new StdClass;
  1125. $update->id = $this->postfields->id;
  1126. $update->deleted = time();
  1127. $update->deleteuserid = mod_forumng_utils::get_real_userid($userid);
  1128. $DB->update_record('forumng_posts', $update);
  1129. $this->postfields->deleted = $update->deleted;
  1130. $this->postfields->deleteuserid = $update->deleteuserid;
  1131. // In case this post is the last one, update the discussion field
  1132. $this->get_discussion()->possible_lastpost_change($this);
  1133. // May result in user becoming incomplete
  1134. $this->update_completion(false);
  1135. if ($log) {
  1136. $this->log('delete post');
  1137. }
  1138. $this->search_update();
  1139. $transaction->allow_commit();
  1140. $this->get_discussion()->uncache();
  1141. }
  1142. /**
  1143. * Marks a post as undeleted.
  1144. * @param bool $log If true, adds entry to Moodle log
  1145. */
  1146. function undelete($log=true) {
  1147. global $DB;
  1148. if (!$this->postfields->deleted) {
  1149. return;
  1150. }
  1151. $transaction = $DB->start_delegated_transaction();
  1152. // Undelete this post
  1153. $update = new StdClass;
  1154. $update->id = $this->postfields->id;
  1155. $update->deleted = 0;
  1156. $update->deleteuserid = 0;
  1157. $DB->update_record('forumng_posts', $update);
  1158. $this->postfields->deleted = 0;
  1159. $this->postfields->deleteuserid = 0;
  1160. // In case this post is the last one, update the discussion field
  1161. $this->get_discussion()->possible_lastpost_change($this);
  1162. // May result in user becoming complete
  1163. $this->update_completion(true);
  1164. if ($log) {
  1165. $this->log('undelete post');
  1166. }
  1167. $this->search_update();
  1168. $transaction->allow_commit();
  1169. $this->get_discussion()->uncache();
  1170. }
  1171. /**
  1172. * Splits this post to become a new discussion
  1173. * @param $newsubject
  1174. * @param bool $log True to log action
  1175. * @return int ID of new discussion
  1176. */
  1177. function split($newsubject, $log=true) {
  1178. global $DB;
  1179. $this->require_children();
  1180. // Begin a transaction
  1181. $transaction = $DB->start_delegated_transaction();
  1182. $olddiscussion = $this->get_discussion();
  1183. // Create new discussion
  1184. $newest = null;
  1185. $this->find_newest_child($newest);
  1186. $newdiscussionid = $olddiscussion->clone_for_split(
  1187. $this->get_id(), $newest->get_id());
  1188. // Update all child posts
  1189. $list = array();
  1190. $this->list_child_ids($list);
  1191. unset($list[0]); // Don't include this post itself
  1192. if (count($list) > 0) {
  1193. list($listsql, $listparams) = mod_forumng_utils::get_in_array_sql('id', $list);
  1194. $DB->execute("
  1195. UPDATE
  1196. {forumng_posts}
  1197. SET
  1198. discussionid = ?
  1199. WHERE
  1200. $listsql", array_merge(array($newdiscussionid), $listparams));
  1201. }
  1202. // Update any edited posts in this discussion. Edited posts are
  1203. // not included …

Large files files are truncated, but you can click here to view the full file