PageRenderTime 58ms CodeModel.GetById 14ms RepoModel.GetById 0ms 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
  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 in the child id list above because they are not
  1204. // loaded as children, but they are conceptually stored as children
  1205. // of one of the posts being moved.
  1206. $parentlist = $list;
  1207. $parentlist[] = $this->get_id();
  1208. list($parentlistsql, $parentlistparams) = mod_forumng_utils::get_in_array_sql(
  1209. 'parentpostid', $parentlist);
  1210. $DB->execute("
  1211. UPDATE
  1212. {forumng_posts}
  1213. SET
  1214. discussionid = ?
  1215. WHERE
  1216. oldversion = 1 AND $parentlistsql", array_merge(array($newdiscussionid), $parentlistparams));
  1217. // Update this post
  1218. $changes = new stdClass;
  1219. $changes->id = $this->get_id();
  1220. $changes->subject = $newsubject;
  1221. $changes->parentpostid = null;
  1222. //When split the post, reset the important to 0 so that it is not highlighted.
  1223. $changes->important = 0;
  1224. // Note don't update modified time, or it makes this post unread,
  1225. // which isn't very helpful
  1226. $changes->discussionid = $newdiscussionid;
  1227. $DB->update_record('forumng_posts', $changes);
  1228. // Update read data if relevant
  1229. if (mod_forumng::enabled_read_tracking() &&
  1230. ($newest->get_modified() >= mod_forumng::get_read_tracking_deadline())) {
  1231. $rs = $DB->get_recordset_sql("
  1232. SELECT
  1233. userid, time
  1234. FROM
  1235. {forumng_read}
  1236. WHERE
  1237. discussionid = ? AND time >= ?", array($olddiscussion->get_id(), $this->get_created()));
  1238. foreach ($rs as $rec) {
  1239. $rec->discussionid = $newdiscussionid;
  1240. $DB->insert_record('forumng_read', $rec);
  1241. }
  1242. $rs->close();
  1243. }
  1244. $olddiscussion->possible_lastpost_change();
  1245. if ($log) {
  1246. $this->log('split post');
  1247. }
  1248. $transaction->allow_commit();
  1249. $this->get_discussion()->uncache();
  1250. // If discussion-based completion is turned on, this may enable someone
  1251. // to complete
  1252. if ($this->get_forum()->get_completion_discussions()) {
  1253. $this->update_completion(true);
  1254. }
  1255. return $newdiscussionid;
  1256. }
  1257. /**
  1258. * Rates this post or updates an existing rating.
  1259. * @param $rating Rating (value depends on scale used) or NO_RATING
  1260. * @param $userid User ID or 0 for current user
  1261. */
  1262. function rate($rating, $userid=0) {
  1263. global $DB;
  1264. $userid = mod_forumng_utils::get_real_userid($userid);
  1265. $transaction = $DB->start_delegated_transaction();
  1266. // Delete any existing rating
  1267. $DB->delete_records('forumng_ratings',
  1268. array('postid' => $this->postfields->id, 'userid' => $userid));
  1269. // Add new rating
  1270. if ($rating != self::NO_RATING) {
  1271. $ratingobj = new StdClass;
  1272. $ratingobj->userid = $userid;
  1273. $ratingobj->postid = $this->postfields->id;
  1274. $ratingobj->time = time();
  1275. $ratingobj->rating = $rating;
  1276. $DB->insert_record('forumng_ratings', $ratingobj);
  1277. }
  1278. // Tell grade to update
  1279. if ($this->get_forum()->get_grading()) {
  1280. $this->get_forum()->update_grades($this->get_user()->id);
  1281. }
  1282. $transaction->allow_commit();
  1283. $this->get_discussion()->uncache();
  1284. }
  1285. /**
  1286. * Records an action in the Moodle log for current user.
  1287. * @param string $action Action name - see datalib.php for suggested verbs
  1288. * and this code for example usage
  1289. * @param int $replyid Specify only when adding a reply; when specified,
  1290. * this is the reply ID (used because the reply entry is logged under
  1291. * the new post, not the old one)
  1292. */
  1293. function log($action, $replyid=0) {
  1294. if ($replyid) {
  1295. $postid = $replyid;
  1296. } else {
  1297. $postid = $this->postfields->id;
  1298. }
  1299. add_to_log($this->get_forum()->get_course_id(), 'forumng',
  1300. $action,
  1301. $this->discussion->get_log_url() . '#p' . $postid,
  1302. $this->postfields->id,
  1303. $this->get_forum()->get_course_module_id());
  1304. }
  1305. // Permissions
  1306. //////////////
  1307. /**
  1308. * Makes security checks for viewing this post. Will not return if
  1309. * user cannot view it.
  1310. * This function should be a complete access check. It calls the
  1311. * discussion's equivalent method.
  1312. * Note that this function only works for the current user when used in
  1313. * interactive mode (ordinary web page view). It cannot be called in cron,
  1314. * web services, etc.
  1315. */
  1316. function require_view() {
  1317. // Check forum and discussion view permission, group access, etc.
  1318. $this->discussion->require_view();
  1319. // Other than being able to view the discussion, no additional
  1320. // requirements to view a normal post
  1321. if (!$this->get_deleted() && !$this->is_old_version()) {
  1322. return true;
  1323. }
  1324. // Deleted posts and old versions of edited posts require viewallposts
  1325. require_capability('mod/forumng:viewallposts',
  1326. $this->get_forum()->get_context());
  1327. }
  1328. /**
  1329. * Checks whether the user can add a new reply to this post, assuming that
  1330. * they can view the discussion.
  1331. * @param string &$whynot
  1332. * @param int $userid
  1333. * @return unknown_type
  1334. */
  1335. function can_reply(&$whynot, $userid=0) {
  1336. $userid = mod_forumng_utils::get_real_userid($userid);
  1337. $context = $this->get_forum()->get_context();
  1338. // Check if post is a special case
  1339. if ($this->get_deleted() || $this->is_old_version()
  1340. || $this->get_discussion()->is_deleted()) {
  1341. $whynot = 'reply_notcurrentpost';
  1342. return false;
  1343. }
  1344. // Check if discussion is different group
  1345. if (!$this->get_discussion()->can_write_to_group()) {
  1346. $whynot = 'reply_wronggroup';
  1347. return false;
  1348. }
  1349. // Check if discussion is locked
  1350. if ($this->get_discussion()->is_locked()) {
  1351. $whynot = 'edit_locked';
  1352. return false;
  1353. }
  1354. // Check read-only dates
  1355. if ($this->get_forum()->is_read_only($userid)) {
  1356. $whynot = 'reply_readonly';
  1357. return false;
  1358. }
  1359. // Check permission
  1360. if (!has_capability('mod/forumng:replypost', $context, $userid)) {
  1361. $whynot = 'reply_nopermission';
  1362. return false;
  1363. }
  1364. // Let forum type veto reply if required
  1365. if (!$this->get_forum()->get_type()->can_reply($this, $userid)) {
  1366. $whynot = 'reply_typelimit';
  1367. return false;
  1368. }
  1369. // Throttling
  1370. if ($this->get_forum()->get_remaining_post_quota($userid) == 0) {
  1371. $whynot = 'reply_postquota';
  1372. return false;
  1373. }
  1374. return true;
  1375. }
  1376. /**
  1377. * @param int $userid User ID or 0 for current
  1378. * @return bool True if user can rate this post
  1379. */
  1380. function can_rate($userid=0) {
  1381. $userid = mod_forumng_utils::get_real_userid($userid);
  1382. return
  1383. !$this->get_deleted() && !$this->is_old_version()
  1384. && !$this->get_discussion()->is_deleted()
  1385. && !$this->get_discussion()->is_locked()
  1386. && $this->get_discussion()->can_write_to_group()
  1387. && $this->get_forum()->can_rate($this->get_created()) &&
  1388. $this->get_user()->id != $userid;
  1389. }
  1390. /**
  1391. * @param int $userid User ID or 0 for current
  1392. * @return bool True if user can view ratings for this post
  1393. */
  1394. function can_view_ratings($userid=0) {
  1395. $userid = mod_forumng_utils::get_real_userid($userid);
  1396. return !$this->get_deleted() && !$this->is_old_version()
  1397. && $this->get_forum()->has_ratings() &&
  1398. has_capability($this->get_user()->id == $userid
  1399. ? 'mod/forumng:viewrating'
  1400. : 'mod/forumng:viewanyrating', $this->get_forum()->get_context());
  1401. }
  1402. function can_split(&$whynot, $userid=0) {
  1403. // Check if this is a special case
  1404. if ($this->get_deleted() || $this->is_old_version()
  1405. || $this->get_discussion()->is_deleted()) {
  1406. $whynot = 'edit_notcurrentpost';
  1407. return false;
  1408. }
  1409. // Check if discussion is different group
  1410. if (!$this->get_discussion()->can_write_to_group()) {
  1411. $whynot = 'edit_wronggroup';
  1412. return false;
  1413. }
  1414. // Can't split root post
  1415. if ($this->is_root_post()) {
  1416. $whynot = 'edit_rootpost';
  1417. return false;
  1418. }
  1419. // Check permission
  1420. if (!$this->get_discussion()->can_split($whynot, $userid)) {
  1421. return false;
  1422. }
  1423. return true;
  1424. }
  1425. /**
  1426. * @param string &$whynot
  1427. * @return bool True if user can alert this post
  1428. */
  1429. function can_alert(&$whynot) {
  1430. // Check if the post has been deleted
  1431. if ($this->get_deleted() || $this->discussion->is_deleted()) {
  1432. $whynot = 'alert_notcurrentpost';
  1433. return false;
  1434. }
  1435. // If not site level or forum level reporting email has been set
  1436. if (!$this->get_forum()->has_reporting_email()) {
  1437. $whynot = 'alert_turnedoff';
  1438. return false;
  1439. }
  1440. return true;
  1441. }
  1442. /**
  1443. * @param string &$whynot
  1444. * @return bool True if can display the direct link
  1445. */
  1446. function can_showdirectlink() {
  1447. // Check if the post has been deleted
  1448. if ($this->get_deleted() || $this->discussion->is_deleted()) {
  1449. return false;
  1450. }
  1451. return true;
  1452. }
  1453. /**
  1454. * Checks whether the user can delete the post, assuming that they can
  1455. * view the discussion.
  1456. * @param string &$whynot If returning false, set to the language string defining
  1457. * reason for not being able to edit
  1458. * @param int $userid User ID or 0 if current
  1459. * @return bool True if user can edit this post
  1460. */
  1461. function can_undelete(&$whynot, $userid=0) {
  1462. $userid = mod_forumng_utils::get_real_userid($userid);
  1463. $context = $this->get_forum()->get_context();
  1464. // Check if post is a special case
  1465. if ($this->is_old_version() || $this->get_discussion()->is_deleted()) {
  1466. $whynot = 'edit_notcurrentpost';
  1467. return false;
  1468. }
  1469. // Check if discussion is different group
  1470. if (!$this->get_discussion()->can_write_to_group()) {
  1471. $whynot = 'edit_wronggroup';
  1472. return false;
  1473. }
  1474. // Check if discussion is locked
  1475. if ($this->get_discussion()->is_locked()) {
  1476. $whynot = 'edit_locked';
  1477. return false;
  1478. }
  1479. if (!$this->get_deleted()) {
  1480. $whynot = 'edit_notdeleted';
  1481. return false;
  1482. }
  1483. // Check the 'edit any' capability (always required to undelete)
  1484. if (!has_capability('mod/forumng:editanypost', $context, $userid)) {
  1485. $whynot = 'edit_nopermission';
  1486. return false;
  1487. }
  1488. // Check read-only dates
  1489. if ($this->get_forum()->is_read_only($userid)) {
  1490. $whynot = 'edit_readonly';
  1491. return false;
  1492. }
  1493. // OK! They're allowed to undelete (whew)
  1494. $whynot = '';
  1495. return true;
  1496. }
  1497. /**
  1498. * Checks whether the user can delete the post, assuming that they can
  1499. * view the discussion.
  1500. * @param string &$whynot If returning false, set to the language string defining
  1501. * reason for not being able to edit
  1502. * @param int $userid User ID or 0 if current
  1503. * @return bool True if user can edit this post
  1504. */
  1505. function can_delete(&$whynot, $userid=0) {
  1506. // At present the logic for this is identical to the edit logic
  1507. // except that you can't delete the root post
  1508. return !$this->is_root_post() && $this->can_edit($whynot, $userid);
  1509. }
  1510. /**
  1511. * Checks whether the user can view edits to posts.
  1512. * @param string $whynot If returning false, set to the language string
  1513. * defining reason for not being able to view edits
  1514. * @param int $userid User ID or 0 for current
  1515. * @return bool True if user can view edits
  1516. */
  1517. function can_view_history(&$whynot, $userid=0) {
  1518. $userid = mod_forumng_utils::get_real_userid($userid);
  1519. // Check the 'edit any' capability
  1520. if (!has_capability('mod/forumng:editanypost',
  1521. $this->get_forum()->get_context(), $userid)) {
  1522. $whynot = 'edit_nopermission';
  1523. return false;
  1524. }
  1525. return true;
  1526. }
  1527. /**
  1528. * Checks whether the user can edit the post, assuming that they can
  1529. * view the discussion.
  1530. * @param string &$whynot If returning false, set to the language string defining
  1531. * reason for not being able to edit
  1532. * @param int $userid User ID or 0 if current
  1533. * @return bool True if user can edit this post
  1534. */
  1535. function can_edit(&$whynot, $userid=0) {
  1536. $userid = mod_forumng_utils::get_real_userid($userid);
  1537. $context = $this->get_forum()->get_context();
  1538. // Check if post is a special case
  1539. if ($this->get_deleted() || $this->is_old_version()
  1540. || $this->get_discussion()->is_deleted()) {
  1541. $whynot = 'edit_notcurrentpost';
  1542. return false;
  1543. }
  1544. // Check if discussion is different group
  1545. if (!$this->get_discussion()->can_write_to_group()) {
  1546. $whynot = 'edit_wronggroup';
  1547. return false;
  1548. }
  1549. // Check if discussion is locked
  1550. if ($this->get_discussion()->is_locked()) {
  1551. $whynot = 'edit_locked';
  1552. return false;
  1553. }
  1554. // Check the 'edit any' capability
  1555. $editanypost = has_capability('mod/forumng:editanypost',
  1556. $context, $userid);
  1557. if (!$editanypost) {
  1558. // If they don't have edit any, they must have either the
  1559. // 'start discussion' or 'reply post' capability (the same
  1560. // one they needed to create the post in the first place)
  1561. if (($this->is_root_post() &&
  1562. !has_capability('mod/forumng:startdiscussion', $context, $userid))
  1563. && (!$this->is_root_post() &&
  1564. !has_capability('mod/forumng:replypost', $context, $userid))) {
  1565. $whynot = 'edit_nopermission';
  1566. return false;
  1567. }
  1568. }
  1569. // Check post belongs to specified user
  1570. if (($this->get_user()->id != $userid) && !$editanypost) {
  1571. $whynot = 'edit_notyours';
  1572. return false;
  1573. }
  1574. // Check editing timeout
  1575. if ((time() > $this->get_edit_time_limit()) && !$editanypost) {
  1576. $whynot = 'edit_timeout';
  1577. return false;
  1578. }
  1579. // Check read-only dates
  1580. if ($this->get_forum()->is_read_only($userid)) {
  1581. $whynot = 'edit_readonly';
  1582. return false;
  1583. }
  1584. // OK! They're allowed to edit (whew)
  1585. $whynot = '';
  1586. return true;
  1587. }
  1588. /**
  1589. * @param int $userid User ID or 0 for current
  1590. * @return True if user can ignore the post editing time limit
  1591. */
  1592. function can_ignore_edit_time_limit($userid=0) {
  1593. $userid = mod_forumng_utils::get_real_userid($userid);
  1594. $context = $this->get_forum()->get_context();
  1595. return has_capability('mod/forumng:editanypost',
  1596. $context, $userid);
  1597. }
  1598. /**
  1599. * @return int Time limit after which users who don't have the edit-all
  1600. * permission are not allowed to edit this post (as epoch value)
  1601. */
  1602. function get_edit_time_limit() {
  1603. global $CFG;
  1604. return $this->get_created() + $CFG->maxeditingtime;
  1605. }
  1606. /**
  1607. * Checks that the user can edit this post - requiring all higher-level
  1608. * access too.
  1609. */
  1610. function require_edit() {
  1611. // Check forum and discussion view permission, group access, etc.
  1612. $this->discussion->require_view();
  1613. // Check post edit
  1614. $whynot = '';
  1615. if (!$this->can_edit($whynot)) {
  1616. print_error($whynot, 'forumng', 'discuss.php?' .
  1617. $this->discussion->get_link_params(mod_forumng::PARAM_HTML));
  1618. }
  1619. }
  1620. /**
  1621. * Checks that the user can reply to this post - requiring all higher-level
  1622. * access too.
  1623. */
  1624. function require_reply() {
  1625. // Check forum and discussion view permission, group access, etc.
  1626. $this->discussion->require_view();
  1627. // Check post reply
  1628. $whynot = '';
  1629. if (!$this->can_reply($whynot)) {
  1630. print_error($whynot, 'forumng', 'discuss.php?' .
  1631. $this->discussion->get_link_params(mod_forumng::PARAM_HTML));
  1632. }
  1633. }
  1634. // Email
  1635. ////////
  1636. /**
  1637. * Obtains a version of this post as an email.
  1638. * @param mod_forumng_post $inreplyto Message this one's replying to, or null
  1639. * if none
  1640. * @param string &$subject Output: Message subject
  1641. * @param string $text Output: Message plain text
  1642. * @param string $html Output: Message HTML (or blank if not in HTML mode)
  1643. * @param bool $ishtml True if in HTML mode
  1644. * @param bool $canreply True if user can reply
  1645. * @param bool $viewfullnames True if user gets to see full names even when
  1646. * these are normally hidden
  1647. * @param string $lang Language of receiving user
  1648. * @param number $timezone Time zone of receiving user
  1649. * @param bool $digest True if in digest mode (does not include parent
  1650. * message or surrounding links).
  1651. * @param bool $discussionemail True if digest is of a single disussion;
  1652. * includes 'post 1' information
  1653. * @param array $extraoptions Set values here to add or override post
  1654. * display options
  1655. */
  1656. function build_email($inreplyto, &$subject, &$text, &$html,
  1657. $ishtml, $canreply, $viewfullnames, $lang, $timezone, $digest=false,
  1658. $discussionemail=false, $extraoptions = array()) {
  1659. global $CFG, $USER;
  1660. $oldlang = $USER->lang;
  1661. $USER->lang = $lang;
  1662. $forum = $this->get_forum();
  1663. $cmid = $forum->get_course_module_id();
  1664. $course = $forum->get_course();
  1665. $discussion = $this->get_discussion();
  1666. // Get subject (may make DB query, unfortunately)
  1667. $subject = $course->shortname . ': ' .
  1668. format_string($this->get_effective_subject(true), true);
  1669. $canunsubscribe = mod_forumng::SUBSCRIPTION_FORCED !=
  1670. $forum->get_effective_subscription_option();
  1671. // Header
  1672. $text = '';
  1673. $html = '';
  1674. if (!$discussionemail && !$digest) {
  1675. $html .= "\n<body id='forumng-email'>\n\n";
  1676. }
  1677. // Navigation bar (breadcrumbs)
  1678. if (!$digest) {
  1679. $text .= $forum->get_course()->shortname . ' -> ';
  1680. $html .= "<div class='forumng-email-navbar'><a target='_blank' " .
  1681. "href='$CFG->wwwroot/course/view.php?id=$course->id'>" .
  1682. "$course->shortname</a> &raquo; ";
  1683. $text .= format_string($forum->get_name(), true);
  1684. $html .= "<a target='_blank' " .
  1685. "href='$CFG->wwwroot/mod/forumng/view.php?" .
  1686. $forum->get_link_params(mod_forumng::PARAM_HTML) . "'>" .
  1687. format_string($forum->get_name(), true) . '</a>';
  1688. // Makes a query :(
  1689. if ($discussionsubject = $discussion->get_subject(true)) {
  1690. $text .= ' -> ' . format_string($discussionsubject, true);
  1691. $html .= " &raquo; <a target='_blank' " .
  1692. "href='$CFG->wwwroot/mod/forumng/discuss.php?" .
  1693. $discussion->get_link_params(mod_forumng::PARAM_HTML) . "'>" .
  1694. format_string($discussionsubject, true).'</a>';
  1695. }
  1696. $html .= '</div>';
  1697. }
  1698. // Main part of email
  1699. $options = array(
  1700. self::OPTION_EMAIL => true,
  1701. self::OPTION_DIGEST => $digest ? true : false,
  1702. self::OPTION_COMMAND_REPLY => ($canreply && !$digest),
  1703. self::OPTION_VIEW_FULL_NAMES => $viewfullnames ? true : false,
  1704. self::OPTION_TIME_ZONE => $timezone,
  1705. self::OPTION_VISIBLE_POST_NUMBERS => $discussionemail,
  1706. self::OPTION_USER_IMAGE => true);
  1707. foreach ($extraoptions as $key => $value) {
  1708. $options[$key] = $value;
  1709. }
  1710. $html .= $this->display(true, $options);
  1711. $displaytext = $this->display(false, $options);
  1712. // In digest, don't display mail divider if mail is blank (== deleted).t
  1713. if ($displaytext !== '' || !$digest) {
  1714. $text .= "\n" . mod_forumng_cron::EMAIL_DIVIDER;
  1715. }
  1716. $text .= $displaytext;
  1717. // Now we need to display the parent post (if any, and if not in digest)
  1718. if ($this->postfields->parentpostid && !$digest) {
  1719. // Print the 'In reply to' heading
  1720. $html .= '<h2>' . get_string('inreplyto', 'forumng') . '</h2>';
  1721. $text .= "\n" . mod_forumng_cron::EMAIL_DIVIDER;
  1722. $text .= get_string('inreplyto', 'forumng'). ":\n\n";
  1723. // Get parent post (unfortunately this requires extra queries)
  1724. $parent = mod_forumng_post::get_from_id(
  1725. $this->postfields->parentpostid,
  1726. $this->get_forum()->get_course_module_id(), false);
  1727. $options = array(
  1728. self::OPTION_EMAIL => true,
  1729. self::OPTION_NO_COMMANDS => true,
  1730. self::OPTION_TIME_ZONE => $timezone);
  1731. foreach ($extraoptions as $key => $value) {
  1732. $options[$key] = $value;
  1733. }
  1734. $html .= $parent->display(true, $options);
  1735. $text .= $parent->display(false, $options);
  1736. }
  1737. if (!$digest && $canunsubscribe) {
  1738. $text .= "\n" . mod_forumng_cron::EMAIL_DIVIDER;
  1739. $text .= get_string("unsubscribe", "forum");
  1740. $text .= ": $CFG->wwwroot/mod/forumng/subscribe.php?" .
  1741. $this->get_forum()->get_link_params(mod_forumng::PARAM_PLAIN) . "\n";
  1742. $html .= "<hr size='1' noshade='noshade' />" .
  1743. "<div class='forumng-email-unsubscribe'>" .
  1744. "<a href='$CFG->wwwroot/mod/forumng/subscribe.php?" .
  1745. $this->get_forum()->get_link_params(mod_forumng::PARAM_HTML) . "'>" .
  1746. get_string('unsubscribe', 'forumng'). '</a></div>';
  1747. }
  1748. if (!$digest && !$discussionemail) {
  1749. $html .= '</body>';
  1750. }
  1751. $USER->lang = $oldlang;
  1752. // If not in HTML mode, chuck away the HTML version
  1753. if (!$ishtml) {
  1754. $html = '';
  1755. }
  1756. }
  1757. // UI
  1758. /////
  1759. /**
  1760. * Displays this post.
  1761. * @param array $html True for HTML format, false for plain text
  1762. * @param array $options See forumngtype::display_post for details
  1763. * @return string HTML or text of post
  1764. */
  1765. function display($html, $options=null) {
  1766. global $USER;
  1767. // Initialise options array
  1768. if (!is_array($options)) {
  1769. $options = array();
  1770. }
  1771. // Default for other options
  1772. if (!array_key_exists(self::OPTION_EMAIL, $options)) {
  1773. $options[self::OPTION_EMAIL] = false;
  1774. }
  1775. if (!array_key_exists(self::OPTION_EXPORT, $options)) {
  1776. $options[self::OPTION_EXPORT] = false;
  1777. }
  1778. if (!array_key_exists(self::OPTION_DIGEST, $options)) {
  1779. $options[self::OPTION_DIGEST] = false;
  1780. }
  1781. if (!array_key_exists(self::OPTION_SINGLE_POST, $options)) {
  1782. $options[self::OPTION_SINGLE_POST] = false;
  1783. }
  1784. if (!array_key_exists(self::OPTION_NO_COMMANDS, $options)) {
  1785. $options[self::OPTION_NO_COMMANDS] = $options[self::OPTION_EXPORT];
  1786. }
  1787. if (!array_key_exists(self::OPTION_COMMAND_REPLY, $options)) {
  1788. $options[self::OPTION_COMMAND_REPLY] =
  1789. !$options[self::OPTION_NO_COMMANDS] && $this->can_reply($junk);
  1790. }
  1791. if (!array_key_exists(self::OPTION_COMMAND_EDIT, $options)) {
  1792. $options[self::OPTION_COMMAND_EDIT] =
  1793. !$options[self::OPTION_NO_COMMANDS] &&
  1794. !$options[self::OPTION_EMAIL] && $this->can_edit($junk);
  1795. }
  1796. if (!array_key_exists(self::OPTION_COMMAND_DELETE, $options)) {
  1797. $options[self::OPTION_COMMAND_DELETE] =
  1798. !$options[self::OPTION_NO_COMMANDS] &&
  1799. !$options[self::OPTION_EMAIL] && $this->can_delete($junk);
  1800. }
  1801. if (!array_key_exists(self::OPTION_COMMAND_REPORT, $options)) {
  1802. $options[self::OPTION_COMMAND_REPORT] =
  1803. !$options[self::OPTION_NO_COMMANDS] &&
  1804. !$options[self::OPTION_EMAIL] && $this->can_alert($junk);
  1805. }
  1806. if (!array_key_exists(self::OPTION_COMMAND_DIRECTLINK, $options)) {
  1807. $options[self::OPTION_COMMAND_DIRECTLINK] =
  1808. !$options[self::OPTION_NO_COMMANDS] && !$options[self::OPTION_EMAIL] &&
  1809. $this->can_showdirectlink();
  1810. }
  1811. if (!array_key_exists(self::OPTION_COMMAND_UNDELETE, $options)) {
  1812. $options[self::OPTION_COMMAND_UNDELETE] =
  1813. !$options[self::OPTION_NO_COMMANDS] &&
  1814. !$options[self::OPTION_EMAIL] && $this->can_undelete($junk);
  1815. }
  1816. if (!array_key_exists(self::OPTION_COMMAND_SPLIT, $options)) {
  1817. $options[self::OPTION_COMMAND_SPLIT] =
  1818. !$options[self::OPTION_NO_COMMANDS] &&
  1819. !$options[self::OPTION_EMAIL] && $this->can_split($junk);
  1820. }
  1821. if (!array_key_exists(self::OPTION_COMMAND_HISTORY, $options)) {
  1822. $options[self::OPTION_COMMAND_HISTORY] =
  1823. !$options[self::OPTION_NO_COMMANDS] &&
  1824. !$options[self::OPTION_EMAIL] && $this->can_view_history($junk);
  1825. }
  1826. if (!array_key_exists(self::OPTION_READ_TIME, $options)) {
  1827. $options[self::OPTION_READ_TIME] = time();
  1828. }
  1829. if (!array_key_exists(self::OPTION_VIEW_FULL_NAMES, $options)) {
  1830. // Default to whether current user has the permission in context
  1831. $options[self::OPTION_VIEW_FULL_NAMES] = has_capability(
  1832. 'moodle/site:viewfullnames', $this->get_forum()->get_context());
  1833. }
  1834. if (!array_key_exists(self::OPTION_TIME_ZONE, $options)) {
  1835. // Default to current user timezone
  1836. $options[self::OPTION_TIME_ZONE] = $USER->timezone;
  1837. }
  1838. if (!array_key_exists(self::OPTION_RATINGS_EDIT, $options)) {
  1839. $options[self::OPTION_RATINGS_EDIT] =
  1840. !$options[self::OPTION_NO_COMMANDS] &&
  1841. !$options[self::OPTION_EMAIL] && $this->can_rate();
  1842. }
  1843. if (!array_key_exists(self::OPTION_EXPANDED, $options)) {
  1844. $options[self::OPTION_EXPANDED] = true;
  1845. }
  1846. if (!array_key_exists(self::OPTION_FLAG_CONTROL, $options)) {
  1847. $options[self::OPTION_FLAG_CONTROL] =
  1848. !$options[self::OPTION_NO_COMMANDS] &&
  1849. !$options[self::OPTION_EMAIL] && $this->can_flag() &&
  1850. $options[self::OPTION_EXPANDED];
  1851. }
  1852. if (!array_key_exists(self::OPTION_VIEW_DELETED_INFO, $options)) {
  1853. $options[self::OPTION_VIEW_DELETED_INFO] =
  1854. $this->can_undelete($junk) && !$options[self::OPTION_EXPORT];
  1855. }
  1856. if (!array_key_exists(self::OPTION_FULL_ADDRESSES, $options)) {
  1857. $options[self::OPTION_FULL_ADDRESSES] =
  1858. $options[self::OPTION_EXPORT] || $options[self::OPTION_EMAIL];
  1859. }
  1860. if (!array_key_exists(self::OPTION_DISCUSSION_SUBJECT, $options)) {
  1861. $options[self::OPTION_DISCUSSION_SUBJECT] = false;
  1862. }
  1863. if (!array_key_exists(self::OPTION_SELECTABLE, $options)) {
  1864. $options[self::OPTION_SELECTABLE] = false;
  1865. }
  1866. if (!array_key_exists(self::OPTION_VISIBLE_POST_NUMBERS, $options)) {
  1867. $options[self::OPTION_VISIBLE_POST_NUMBERS] = false;
  1868. }
  1869. if (!array_key_exists(self::OPTION_USER_IMAGE, $options)) {
  1870. $options[self::OPTION_USER_IMAGE] = true;
  1871. }
  1872. if (!array_key_exists(self::OPTION_PRINTABLE_VERSION, $options)) {
  1873. $options[self::OPTION_PRINTABLE_VERSION] = false;
  1874. }
  1875. if (!array_key_exists(self::OPTION_RATINGS_VIEW, $options)) {
  1876. $options[self::OPTION_RATINGS_VIEW] =
  1877. ((!$options[self::OPTION_NO_COMMANDS] && !$options[self::OPTION_EMAIL]) ||
  1878. $options[self::OPTION_PRINTABLE_VERSION]) &&
  1879. $this->can_view_ratings();
  1880. }
  1881. $dojumps = !$options[self::OPTION_NO_COMMANDS] && !$options[self::OPTION_EMAIL] &&
  1882. !$options[self::OPTION_SINGLE_POST];
  1883. if (!array_key_exists(self::OPTION_JUMP_NEXT, $options)) {
  1884. $options[self::OPTION_JUMP_NEXT] =
  1885. ($dojumps && $this->is_unread() && ($next=$this->get_next_unread()))
  1886. ? $next->get_id() : null;
  1887. }
  1888. if (!array_key_exists(self::OPTION_JUMP_PREVIOUS, $options)) {
  1889. $options[self::OPTION_JUMP_PREVIOUS] =
  1890. ($dojumps && $this->is_unread() && $this->get_previous_unread())
  1891. ? $this->get_previous_unread()->get_id() : null;
  1892. }
  1893. if (!array_key_exists(self::OPTION_JUMP_PARENT, $options)) {
  1894. $options[self::OPTION_JUMP_PARENT] =
  1895. ($dojumps && !$this->is_root_post()) ? $this->get_parent()->get_id() : null;
  1896. }
  1897. if (!array_key_exists(self::OPTION_FIRST_UNREAD, $options)) {
  1898. $options[self::OPTION_FIRST_UNREAD] = !$options[self::OPTION_EMAIL] &&
  1899. !$options[self::OPTION_SINGLE_POST] && $this->is_unread() &&
  1900. !$this->get_previous_unread();
  1901. }
  1902. if (!array_key_exists(self::OPTION_UNREAD_NOT_HIGHLIGHTED, $options)) {
  1903. $options[self::OPTION_UNREAD_NOT_HIGHLIGHTED] = false;
  1904. }
  1905. // Get forum type to do actual display
  1906. $out = mod_forumng_utils::get_renderer();
  1907. return $out->render_post($this, $html, $options);
  1908. }
  1909. function display_with_children($options = null, $recursing = false) {
  1910. global $USER;
  1911. $this->require_children();
  1912. if (!$recursing) {
  1913. // Initialise options array
  1914. if (!is_array($options)) {
  1915. $options = array();
  1916. }
  1917. if (!array_key_exists(self::OPTION_EXPORT, $options)) {
  1918. $options[self::OPTION_EXPORT] = false;
  1919. }
  1920. if (!array_key_exists(self::OPTION_CHILDREN_EXPANDED, $options)) {
  1921. $options[self::OPTION_CHILDREN_EXPANDED] =
  1922. $options[self::OPTION_EXPORT];
  1923. }
  1924. if (!array_key_exists(self::OPTION_CHILDREN_COLLAPSED, $options)) {
  1925. $options[self::OPTION_CHILDREN_COLLAPSED] = false;
  1926. }
  1927. if (!array_key_exists(self::OPTION_INCLUDE_LOCK, $options)) {
  1928. $options[self::OPTION_INCLUDE_LOCK] = false;
  1929. }
  1930. }
  1931. $export = $options[self::OPTION_EXPORT];
  1932. // Decide ID of locked post to hide (if any)
  1933. if ($this->discussion->is_locked() &&
  1934. !$options[self::OPTION_INCLUDE_LOCK]) {
  1935. $lockpostid = $this->discussion->get_last_post_id();
  1936. } else {
  1937. $lockpostid = 0;
  1938. }
  1939. // Display this post. It should be 'short' unless it is unread, parent
  1940. // of unread post, top post, or flagged
  1941. $options[self::OPTION_EXPANDED] = !$recursing ||
  1942. ( !$options[self::OPTION_CHILDREN_COLLAPSED] &&
  1943. ($this->is_unread()
  1944. || $this->is_flagged()
  1945. || $this->has_unread_child() || $this->forceexpand || !$recursing
  1946. || $options[self::OPTION_CHILDREN_EXPANDED]));
  1947. $output = $this->display(true, $options);
  1948. // Are there any children?
  1949. if (count($this->children) > 0 && !($lockpostid
  1950. && count($this->children)==1
  1951. && reset($this->children)->get_id()==$lockpostid)) {
  1952. $output .= $export ? '<blockquote>' : '<div class="forumng-replies">';
  1953. foreach ($this->children as $child) {
  1954. if ($child->get_id()!=$lockpostid) {
  1955. $output .= $child->display_with_children($options, true);
  1956. }
  1957. }
  1958. $output .= $export ? '</blockquote>' : '</div>';
  1959. }
  1960. if (!$recursing) {
  1961. $out = mod_forumng_utils::get_renderer();
  1962. $output = $out->render_post_group($this->get_discussion(), $output);
  1963. }
  1964. return $output;
  1965. }
  1966. /** @return string User picture HTML (for post author) */
  1967. function display_user_picture() {
  1968. $out = mod_forumng_utils::get_renderer();
  1969. return $out->user_picture($this->get_user(),
  1970. array('courseid'=>$this->get_forum()->get_course_id()));
  1971. }
  1972. /**
  1973. * Displays group pictures. This may make a (single) DB query if group
  1974. * data has not yet been retrieved for this discussion.
  1975. * @return string Group pictures HTML (empty string if none) for groups
  1976. * that post author belongs to
  1977. */
  1978. function display_group_pictures() {
  1979. $groups = $this->discussion->get_user_groups($this->get_user()->id);
  1980. if (count($groups) == 0) {
  1981. return '';
  1982. }
  1983. return print_group_picture($groups, $this->get_forum()->get_course_id(),
  1984. false, true);
  1985. }
  1986. /**
  1987. * Displays this draft as an item on the list.
  1988. * @param bool $last True if this is last in list
  1989. * @return string HTML code for the item
  1990. */
  1991. public function display_flagged_list_item($last) {
  1992. return $this->get_forum()->get_type()->display_flagged_list_item(
  1993. $this, $last);
  1994. }
  1995. /**
  1996. * Describes the post fields in JSON format. This is used for the AJAX
  1997. * edit code.
  1998. * @return string JSON structure listing key post fields.
  1999. */
  2000. function prepare_edit_json() {
  2001. global $USER;
  2002. $forum = $this->get_forum();
  2003. $filecontext = $forum->get_context(true);
  2004. $fileoptions = array('subdirs'=>false, 'maxbytes'=>$forum->get_max_bytes());
  2005. // Prepare draft area for attachments
  2006. $draftitemid = 0;
  2007. file_prepare_draft_area($draftitemid, $filecontext->id, 'mod_forumng', 'attachment',
  2008. $this->get_id(), $fileoptions);
  2009. // Prepare draft area for message files
  2010. $messagedraftitemid = 0;
  2011. $message = $this->get_raw_message();
  2012. $message = file_prepare_draft_area($messagedraftitemid, $filecontext->id, 'mod_forumng',
  2013. 'message', $this->get_id(), $fileoptions, $message);
  2014. // Get list of files for main attachment area
  2015. $options = file_get_drafarea_files($draftitemid, '/');
  2016. $usercontext = context_user::instance($USER->id);
  2017. $fs = get_file_storage();
  2018. $files = $fs->get_area_files($usercontext->id, 'user', 'draft',
  2019. $options->itemid, 'id', false);
  2020. $options->filecount = count($files);
  2021. // Get list of files for message area
  2022. $messageoptions = file_get_drafarea_files($messagedraftitemid, '/');
  2023. $files = $fs->get_area_files($usercontext->id, 'user', 'draft',
  2024. $messageoptions->itemid, 'id', false);
  2025. $messageoptions->filecount = count($files);
  2026. // Put everything together with basic data
  2027. $basicvalues = (object)array('subject'=>$this->get_subject(),
  2028. 'message'=>$message, 'format'=>$this->get_format(),
  2029. 'setimportant'=>$this->is_important() ? 1 : 0);
  2030. $basicvalues->options = $options;
  2031. $basicvalues->messageoptions = $messageoptions;
  2032. // Add time limit info
  2033. $timelimit = $this->can_ignore_edit_time_limit()
  2034. ? 0 : $this->get_edit_time_limit();
  2035. if ($timelimit) {
  2036. $basicvalues->editlimit = $timelimit-time();
  2037. $basicvalues->editlimitmsg = get_string('editlimited', 'forumng',
  2038. userdate($timelimit-30, get_string('strftimetime', 'langconfig')));
  2039. } else {
  2040. $basicvalues->editlimit = 0;
  2041. }
  2042. // JSON encoding
  2043. return json_encode($basicvalues);
  2044. }
  2045. /**
  2046. * Prints AJAX version of the post to output, and exits.
  2047. * @param mixed $postorid Post object or ID of post
  2048. * @param int $cloneid If $postorid is an id, a clone id may be necessary
  2049. * to construct the post
  2050. * @param array $options Post options if any
  2051. * @param int $postid ID of post
  2052. */
  2053. public static function print_for_ajax_and_exit($postorid, $cloneid=null,
  2054. $options=array()) {
  2055. if (is_object($postorid)) {
  2056. $post = $postorid;
  2057. } else {
  2058. $post = mod_forumng_post::get_from_id($postorid, $cloneid, true);
  2059. }
  2060. header('Content-Type: text/plain');
  2061. print trim($post->display(true, $options));
  2062. exit;
  2063. }
  2064. // Completion
  2065. /////////////
  2066. public function update_completion($positive) {
  2067. // Do nothing if completion isn't enabled
  2068. if (!$this->get_forum()->is_auto_completion_enabled(true)) {
  2069. return;
  2070. }
  2071. $course = $this->get_forum()->get_course();
  2072. $cm = $this->get_forum()->get_course_module();
  2073. $completion = new completion_info($course);
  2074. $completion->update_state($cm, $positive ? COMPLETION_COMPLETE : COMPLETION_INCOMPLETE,
  2075. $this->postfields->userid);
  2076. }
  2077. }