Search moodle.org's
Developer Documentation


/mod/forum/ -> lib.php (source)
   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * @package   mod_forum
  19   * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
  20   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  21   */
  22  
  23  defined('MOODLE_INTERNAL') || die();
  24  
  25  /** Include required files */
  26  require_once (__DIR__ . '/deprecatedlib.php');
  27  require_once($CFG->libdir.'/filelib.php');
  28  require_once($CFG->libdir.'/eventslib.php');
  29  
  30  /// CONSTANTS ///////////////////////////////////////////////////////////
  31  
  32  define('FORUM_MODE_FLATOLDEST', 1);
  33  define('FORUM_MODE_FLATNEWEST', -1);
  34  define('FORUM_MODE_THREADED', 2);
  35  define('FORUM_MODE_NESTED', 3);
  36  
  37  define('FORUM_CHOOSESUBSCRIBE', 0);
  38  define('FORUM_FORCESUBSCRIBE', 1);
  39  define('FORUM_INITIALSUBSCRIBE', 2);
  40  define('FORUM_DISALLOWSUBSCRIBE',3);
  41  
  42  /**
  43   * FORUM_TRACKING_OFF - Tracking is not available for this forum.
  44   */
  45  define('FORUM_TRACKING_OFF', 0);
  46  
  47  /**
  48   * FORUM_TRACKING_OPTIONAL - Tracking is based on user preference.
  49   */
  50  define('FORUM_TRACKING_OPTIONAL', 1);
  51  
  52  /**
  53   * FORUM_TRACKING_FORCED - Tracking is on, regardless of user setting.
  54   * Treated as FORUM_TRACKING_OPTIONAL if $CFG->forum_allowforcedreadtracking is off.
  55   */
  56  define('FORUM_TRACKING_FORCED', 2);
  57  
  58  define('FORUM_MAILED_PENDING', 0);
  59  define('FORUM_MAILED_SUCCESS', 1);
  60  define('FORUM_MAILED_ERROR', 2);
  61  
  62  if (!defined('FORUM_CRON_USER_CACHE')) {
  63      /** Defines how many full user records are cached in forum cron. */
  64      define('FORUM_CRON_USER_CACHE', 5000);
  65  }
  66  
  67  /// STANDARD FUNCTIONS ///////////////////////////////////////////////////////////
  68  
  69  /**
  70   * Given an object containing all the necessary data,
  71   * (defined by the form in mod_form.php) this function
  72   * will create a new instance and return the id number
  73   * of the new instance.
  74   *
  75   * @param stdClass $forum add forum instance
  76   * @param mod_forum_mod_form $mform
  77   * @return int intance id
  78   */
  79  function forum_add_instance($forum, $mform = null) {
  80      global $CFG, $DB;
  81  
  82      $forum->timemodified = time();
  83  
  84      if (empty($forum->assessed)) {
  85          $forum->assessed = 0;
  86      }
  87  
  88      if (empty($forum->ratingtime) or empty($forum->assessed)) {
  89          $forum->assesstimestart  = 0;
  90          $forum->assesstimefinish = 0;
  91      }
  92  
  93      $forum->id = $DB->insert_record('forum', $forum);
  94      $modcontext = context_module::instance($forum->coursemodule);
  95  
  96      if ($forum->type == 'single') {  // Create related discussion.
  97          $discussion = new stdClass();
  98          $discussion->course        = $forum->course;
  99          $discussion->forum         = $forum->id;
 100          $discussion->name          = $forum->name;
 101          $discussion->assessed      = $forum->assessed;
 102          $discussion->message       = $forum->intro;
 103          $discussion->messageformat = $forum->introformat;
 104          $discussion->messagetrust  = trusttext_trusted(context_course::instance($forum->course));
 105          $discussion->mailnow       = false;
 106          $discussion->groupid       = -1;
 107  
 108          $message = '';
 109  
 110          $discussion->id = forum_add_discussion($discussion, null, $message);
 111  
 112          if ($mform and $draftid = file_get_submitted_draft_itemid('introeditor')) {
 113              // Ugly hack - we need to copy the files somehow.
 114              $discussion = $DB->get_record('forum_discussions', array('id'=>$discussion->id), '*', MUST_EXIST);
 115              $post = $DB->get_record('forum_posts', array('id'=>$discussion->firstpost), '*', MUST_EXIST);
 116  
 117              $options = array('subdirs'=>true); // Use the same options as intro field!
 118              $post->message = file_save_draft_area_files($draftid, $modcontext->id, 'mod_forum', 'post', $post->id, $options, $post->message);
 119              $DB->set_field('forum_posts', 'message', $post->message, array('id'=>$post->id));
 120          }
 121      }
 122  
 123      forum_grade_item_update($forum);
 124  
 125      return $forum->id;
 126  }
 127  
 128  /**
 129   * Handle changes following the creation of a forum instance.
 130   * This function is typically called by the course_module_created observer.
 131   *
 132   * @param object $context the forum context
 133   * @param stdClass $forum The forum object
 134   * @return void
 135   */
 136  function forum_instance_created($context, $forum) {
 137      if ($forum->forcesubscribe == FORUM_INITIALSUBSCRIBE) {
 138          $users = \mod_forum\subscriptions::get_potential_subscribers($context, 0, 'u.id, u.email');
 139          foreach ($users as $user) {
 140              \mod_forum\subscriptions::subscribe_user($user->id, $forum, $context);
 141          }
 142      }
 143  }
 144  
 145  /**
 146   * Given an object containing all the necessary data,
 147   * (defined by the form in mod_form.php) this function
 148   * will update an existing instance with new data.
 149   *
 150   * @global object
 151   * @param object $forum forum instance (with magic quotes)
 152   * @return bool success
 153   */
 154  function forum_update_instance($forum, $mform) {
 155      global $DB, $OUTPUT, $USER;
 156  
 157      $forum->timemodified = time();
 158      $forum->id           = $forum->instance;
 159  
 160      if (empty($forum->assessed)) {
 161          $forum->assessed = 0;
 162      }
 163  
 164      if (empty($forum->ratingtime) or empty($forum->assessed)) {
 165          $forum->assesstimestart  = 0;
 166          $forum->assesstimefinish = 0;
 167      }
 168  
 169      $oldforum = $DB->get_record('forum', array('id'=>$forum->id));
 170  
 171      // MDL-3942 - if the aggregation type or scale (i.e. max grade) changes then recalculate the grades for the entire forum
 172      // if  scale changes - do we need to recheck the ratings, if ratings higher than scale how do we want to respond?
 173      // for count and sum aggregation types the grade we check to make sure they do not exceed the scale (i.e. max score) when calculating the grade
 174      if (($oldforum->assessed<>$forum->assessed) or ($oldforum->scale<>$forum->scale)) {
 175          forum_update_grades($forum); // recalculate grades for the forum
 176      }
 177  
 178      if ($forum->type == 'single') {  // Update related discussion and post.
 179          $discussions = $DB->get_records('forum_discussions', array('forum'=>$forum->id), 'timemodified ASC');
 180          if (!empty($discussions)) {
 181              if (count($discussions) > 1) {
 182                  echo $OUTPUT->notification(get_string('warnformorepost', 'forum'));
 183              }
 184              $discussion = array_pop($discussions);
 185          } else {
 186              // try to recover by creating initial discussion - MDL-16262
 187              $discussion = new stdClass();
 188              $discussion->course          = $forum->course;
 189              $discussion->forum           = $forum->id;
 190              $discussion->name            = $forum->name;
 191              $discussion->assessed        = $forum->assessed;
 192              $discussion->message         = $forum->intro;
 193              $discussion->messageformat   = $forum->introformat;
 194              $discussion->messagetrust    = true;
 195              $discussion->mailnow         = false;
 196              $discussion->groupid         = -1;
 197  
 198              $message = '';
 199  
 200              forum_add_discussion($discussion, null, $message);
 201  
 202              if (! $discussion = $DB->get_record('forum_discussions', array('forum'=>$forum->id))) {
 203                  print_error('cannotadd', 'forum');
 204              }
 205          }
 206          if (! $post = $DB->get_record('forum_posts', array('id'=>$discussion->firstpost))) {
 207              print_error('cannotfindfirstpost', 'forum');
 208          }
 209  
 210          $cm         = get_coursemodule_from_instance('forum', $forum->id);
 211          $modcontext = context_module::instance($cm->id, MUST_EXIST);
 212  
 213          $post = $DB->get_record('forum_posts', array('id'=>$discussion->firstpost), '*', MUST_EXIST);
 214          $post->subject       = $forum->name;
 215          $post->message       = $forum->intro;
 216          $post->messageformat = $forum->introformat;
 217          $post->messagetrust  = trusttext_trusted($modcontext);
 218          $post->modified      = $forum->timemodified;
 219          $post->userid        = $USER->id;    // MDL-18599, so that current teacher can take ownership of activities.
 220  
 221          if ($mform and $draftid = file_get_submitted_draft_itemid('introeditor')) {
 222              // Ugly hack - we need to copy the files somehow.
 223              $options = array('subdirs'=>true); // Use the same options as intro field!
 224              $post->message = file_save_draft_area_files($draftid, $modcontext->id, 'mod_forum', 'post', $post->id, $options, $post->message);
 225          }
 226  
 227          $DB->update_record('forum_posts', $post);
 228          $discussion->name = $forum->name;
 229          $DB->update_record('forum_discussions', $discussion);
 230      }
 231  
 232      $DB->update_record('forum', $forum);
 233  
 234      $modcontext = context_module::instance($forum->coursemodule);
 235      if (($forum->forcesubscribe == FORUM_INITIALSUBSCRIBE) && ($oldforum->forcesubscribe <> $forum->forcesubscribe)) {
 236          $users = \mod_forum\subscriptions::get_potential_subscribers($modcontext, 0, 'u.id, u.email', '');
 237          foreach ($users as $user) {
 238              \mod_forum\subscriptions::subscribe_user($user->id, $forum, $modcontext);
 239          }
 240      }
 241  
 242      forum_grade_item_update($forum);
 243  
 244      return true;
 245  }
 246  
 247  
 248  /**
 249   * Given an ID of an instance of this module,
 250   * this function will permanently delete the instance
 251   * and any data that depends on it.
 252   *
 253   * @global object
 254   * @param int $id forum instance id
 255   * @return bool success
 256   */
 257  function forum_delete_instance($id) {
 258      global $DB;
 259  
 260      if (!$forum = $DB->get_record('forum', array('id'=>$id))) {
 261          return false;
 262      }
 263      if (!$cm = get_coursemodule_from_instance('forum', $forum->id)) {
 264          return false;
 265      }
 266      if (!$course = $DB->get_record('course', array('id'=>$cm->course))) {
 267          return false;
 268      }
 269  
 270      $context = context_module::instance($cm->id);
 271  
 272      // now get rid of all files
 273      $fs = get_file_storage();
 274      $fs->delete_area_files($context->id);
 275  
 276      $result = true;
 277  
 278      // Delete digest and subscription preferences.
 279      $DB->delete_records('forum_digests', array('forum' => $forum->id));
 280      $DB->delete_records('forum_subscriptions', array('forum'=>$forum->id));
 281      $DB->delete_records('forum_discussion_subs', array('forum' => $forum->id));
 282  
 283      if ($discussions = $DB->get_records('forum_discussions', array('forum'=>$forum->id))) {
 284          foreach ($discussions as $discussion) {
 285              if (!forum_delete_discussion($discussion, true, $course, $cm, $forum)) {
 286                  $result = false;
 287              }
 288          }
 289      }
 290  
 291      forum_tp_delete_read_records(-1, -1, -1, $forum->id);
 292  
 293      if (!$DB->delete_records('forum', array('id'=>$forum->id))) {
 294          $result = false;
 295      }
 296  
 297      forum_grade_item_delete($forum);
 298  
 299      return $result;
 300  }
 301  
 302  
 303  /**
 304   * Indicates API features that the forum supports.
 305   *
 306   * @uses FEATURE_GROUPS
 307   * @uses FEATURE_GROUPINGS
 308   * @uses FEATURE_MOD_INTRO
 309   * @uses FEATURE_COMPLETION_TRACKS_VIEWS
 310   * @uses FEATURE_COMPLETION_HAS_RULES
 311   * @uses FEATURE_GRADE_HAS_GRADE
 312   * @uses FEATURE_GRADE_OUTCOMES
 313   * @param string $feature
 314   * @return mixed True if yes (some features may use other values)
 315   */
 316  function forum_supports($feature) {
 317      switch($feature) {
 318          case FEATURE_GROUPS:                  return true;
 319          case FEATURE_GROUPINGS:               return true;
 320          case FEATURE_MOD_INTRO:               return true;
 321          case FEATURE_COMPLETION_TRACKS_VIEWS: return true;
 322          case FEATURE_COMPLETION_HAS_RULES:    return true;
 323          case FEATURE_GRADE_HAS_GRADE:         return true;
 324          case FEATURE_GRADE_OUTCOMES:          return true;
 325          case FEATURE_RATE:                    return true;
 326          case FEATURE_BACKUP_MOODLE2:          return true;
 327          case FEATURE_SHOW_DESCRIPTION:        return true;
 328          case FEATURE_PLAGIARISM:              return true;
 329  
 330          default: return null;
 331      }
 332  }
 333  
 334  
 335  /**
 336   * Obtains the automatic completion state for this forum based on any conditions
 337   * in forum settings.
 338   *
 339   * @global object
 340   * @global object
 341   * @param object $course Course
 342   * @param object $cm Course-module
 343   * @param int $userid User ID
 344   * @param bool $type Type of comparison (or/and; can be used as return value if no conditions)
 345   * @return bool True if completed, false if not. (If no conditions, then return
 346   *   value depends on comparison type)
 347   */
 348  function forum_get_completion_state($course,$cm,$userid,$type) {
 349      global $CFG,$DB;
 350  
 351      // Get forum details
 352      if (!($forum=$DB->get_record('forum',array('id'=>$cm->instance)))) {
 353          throw new Exception("Can't find forum {$cm->instance}");
 354      }
 355  
 356      $result=$type; // Default return value
 357  
 358      $postcountparams=array('userid'=>$userid,'forumid'=>$forum->id);
 359      $postcountsql="
 360  SELECT
 361      COUNT(1)
 362  FROM
 363      {forum_posts} fp
 364      INNER JOIN {forum_discussions} fd ON fp.discussion=fd.id
 365  WHERE
 366      fp.userid=:userid AND fd.forum=:forumid";
 367  
 368      if ($forum->completiondiscussions) {
 369          $value = $forum->completiondiscussions <=
 370                   $DB->count_records('forum_discussions',array('forum'=>$forum->id,'userid'=>$userid));
 371          if ($type == COMPLETION_AND) {
 372              $result = $result && $value;
 373          } else {
 374              $result = $result || $value;
 375          }
 376      }
 377      if ($forum->completionreplies) {
 378          $value = $forum->completionreplies <=
 379                   $DB->get_field_sql( $postcountsql.' AND fp.parent<>0',$postcountparams);
 380          if ($type==COMPLETION_AND) {
 381              $result = $result && $value;
 382          } else {
 383              $result = $result || $value;
 384          }
 385      }
 386      if ($forum->completionposts) {
 387          $value = $forum->completionposts <= $DB->get_field_sql($postcountsql,$postcountparams);
 388          if ($type == COMPLETION_AND) {
 389              $result = $result && $value;
 390          } else {
 391              $result = $result || $value;
 392          }
 393      }
 394  
 395      return $result;
 396  }
 397  
 398  /**
 399   * Create a message-id string to use in the custom headers of forum notification emails
 400   *
 401   * message-id is used by email clients to identify emails and to nest conversations
 402   *
 403   * @param int $postid The ID of the forum post we are notifying the user about
 404   * @param int $usertoid The ID of the user being notified
 405   * @param string $hostname The server's hostname
 406   * @return string A unique message-id
 407   */
 408  function forum_get_email_message_id($postid, $usertoid, $hostname) {
 409      return '<'.hash('sha256',$postid.'to'.$usertoid).'@'.$hostname.'>';
 410  }
 411  
 412  /**
 413   * Removes properties from user record that are not necessary
 414   * for sending post notifications.
 415   * @param stdClass $user
 416   * @return void, $user parameter is modified
 417   */
 418  function forum_cron_minimise_user_record(stdClass $user) {
 419  
 420      // We store large amount of users in one huge array,
 421      // make sure we do not store info there we do not actually need
 422      // in mail generation code or messaging.
 423  
 424      unset($user->institution);
 425      unset($user->department);
 426      unset($user->address);
 427      unset($user->city);
 428      unset($user->url);
 429      unset($user->currentlogin);
 430      unset($user->description);
 431      unset($user->descriptionformat);
 432  }
 433  
 434  /**
 435   * Function to be run periodically according to the scheduled task.
 436   *
 437   * Finds all posts that have yet to be mailed out, and mails them
 438   * out to all subscribers as well as other maintance tasks.
 439   *
 440   * NOTE: Since 2.7.2 this function is run by scheduled task rather
 441   * than standard cron.
 442   *
 443   * @todo MDL-44734 The function will be split up into seperate tasks.
 444   */
 445  function forum_cron() {
 446      global $CFG, $USER, $DB;
 447  
 448      $site = get_site();
 449  
 450      // All users that are subscribed to any post that needs sending,
 451      // please increase $CFG->extramemorylimit on large sites that
 452      // send notifications to a large number of users.
 453      $users = array();
 454      $userscount = 0; // Cached user counter - count($users) in PHP is horribly slow!!!
 455  
 456      // Status arrays.
 457      $mailcount  = array();
 458      $errorcount = array();
 459  
 460      // caches
 461      $discussions        = array();
 462      $forums             = array();
 463      $courses            = array();
 464      $coursemodules      = array();
 465      $subscribedusers    = array();
 466      $messageinboundhandlers = array();
 467  
 468      // Posts older than 2 days will not be mailed.  This is to avoid the problem where
 469      // cron has not been running for a long time, and then suddenly people are flooded
 470      // with mail from the past few weeks or months
 471      $timenow   = time();
 472      $endtime   = $timenow - $CFG->maxeditingtime;
 473      $starttime = $endtime - 48 * 3600;   // Two days earlier
 474  
 475      // Get the list of forum subscriptions for per-user per-forum maildigest settings.
 476      $digestsset = $DB->get_recordset('forum_digests', null, '', 'id, userid, forum, maildigest');
 477      $digests = array();
 478      foreach ($digestsset as $thisrow) {
 479          if (!isset($digests[$thisrow->forum])) {
 480              $digests[$thisrow->forum] = array();
 481          }
 482          $digests[$thisrow->forum][$thisrow->userid] = $thisrow->maildigest;
 483      }
 484      $digestsset->close();
 485  
 486      // Create the generic messageinboundgenerator.
 487      $messageinboundgenerator = new \core\message\inbound\address_manager();
 488      $messageinboundgenerator->set_handler('\mod_forum\message\inbound\reply_handler');
 489  
 490      if ($posts = forum_get_unmailed_posts($starttime, $endtime, $timenow)) {
 491          // Mark them all now as being mailed.  It's unlikely but possible there
 492          // might be an error later so that a post is NOT actually mailed out,
 493          // but since mail isn't crucial, we can accept this risk.  Doing it now
 494          // prevents the risk of duplicated mails, which is a worse problem.
 495  
 496          if (!forum_mark_old_posts_as_mailed($endtime)) {
 497              mtrace('Errors occurred while trying to mark some posts as being mailed.');
 498              return false;  // Don't continue trying to mail them, in case we are in a cron loop
 499          }
 500  
 501          // checking post validity, and adding users to loop through later
 502          foreach ($posts as $pid => $post) {
 503  
 504              $discussionid = $post->discussion;
 505              if (!isset($discussions[$discussionid])) {
 506                  if ($discussion = $DB->get_record('forum_discussions', array('id'=> $post->discussion))) {
 507                      $discussions[$discussionid] = $discussion;
 508                      \mod_forum\subscriptions::fill_subscription_cache($discussion->forum);
 509                      \mod_forum\subscriptions::fill_discussion_subscription_cache($discussion->forum);
 510  
 511                  } else {
 512                      mtrace('Could not find discussion ' . $discussionid);
 513                      unset($posts[$pid]);
 514                      continue;
 515                  }
 516              }
 517              $forumid = $discussions[$discussionid]->forum;
 518              if (!isset($forums[$forumid])) {
 519                  if ($forum = $DB->get_record('forum', array('id' => $forumid))) {
 520                      $forums[$forumid] = $forum;
 521                  } else {
 522                      mtrace('Could not find forum '.$forumid);
 523                      unset($posts[$pid]);
 524                      continue;
 525                  }
 526              }
 527              $courseid = $forums[$forumid]->course;
 528              if (!isset($courses[$courseid])) {
 529                  if ($course = $DB->get_record('course', array('id' => $courseid))) {
 530                      $courses[$courseid] = $course;
 531                  } else {
 532                      mtrace('Could not find course '.$courseid);
 533                      unset($posts[$pid]);
 534                      continue;
 535                  }
 536              }
 537              if (!isset($coursemodules[$forumid])) {
 538                  if ($cm = get_coursemodule_from_instance('forum', $forumid, $courseid)) {
 539                      $coursemodules[$forumid] = $cm;
 540                  } else {
 541                      mtrace('Could not find course module for forum '.$forumid);
 542                      unset($posts[$pid]);
 543                      continue;
 544                  }
 545              }
 546  
 547              // Save the Inbound Message datakey here to reduce DB queries later.
 548              $messageinboundgenerator->set_data($pid);
 549              $messageinboundhandlers[$pid] = $messageinboundgenerator->fetch_data_key();
 550  
 551              // Caching subscribed users of each forum.
 552              if (!isset($subscribedusers[$forumid])) {
 553                  $modcontext = context_module::instance($coursemodules[$forumid]->id);
 554                  if ($subusers = \mod_forum\subscriptions::fetch_subscribed_users($forums[$forumid], 0, $modcontext, 'u.*', true)) {
 555  
 556                      foreach ($subusers as $postuser) {
 557                          // this user is subscribed to this forum
 558                          $subscribedusers[$forumid][$postuser->id] = $postuser->id;
 559                          $userscount++;
 560                          if ($userscount > FORUM_CRON_USER_CACHE) {
 561                              // Store minimal user info.
 562                              $minuser = new stdClass();
 563                              $minuser->id = $postuser->id;
 564                              $users[$postuser->id] = $minuser;
 565                          } else {
 566                              // Cache full user record.
 567                              forum_cron_minimise_user_record($postuser);
 568                              $users[$postuser->id] = $postuser;
 569                          }
 570                      }
 571                      // Release memory.
 572                      unset($subusers);
 573                      unset($postuser);
 574                  }
 575              }
 576              $mailcount[$pid] = 0;
 577              $errorcount[$pid] = 0;
 578          }
 579      }
 580  
 581      if ($users && $posts) {
 582  
 583          $urlinfo = parse_url($CFG->wwwroot);
 584          $hostname = $urlinfo['host'];
 585  
 586          foreach ($users as $userto) {
 587              // Terminate if processing of any account takes longer than 2 minutes.
 588              core_php_time_limit::raise(120);
 589  
 590              mtrace('Processing user ' . $userto->id);
 591  
 592              // Init user caches - we keep the cache for one cycle only, otherwise it could consume too much memory.
 593              if (isset($userto->username)) {
 594                  $userto = clone($userto);
 595              } else {
 596                  $userto = $DB->get_record('user', array('id' => $userto->id));
 597                  forum_cron_minimise_user_record($userto);
 598              }
 599              $userto->viewfullnames = array();
 600              $userto->canpost       = array();
 601              $userto->markposts     = array();
 602  
 603              // Setup this user so that the capabilities are cached, and environment matches receiving user.
 604              cron_setup_user($userto);
 605  
 606              // Reset the caches.
 607              foreach ($coursemodules as $forumid => $unused) {
 608                  $coursemodules[$forumid]->cache       = new stdClass();
 609                  $coursemodules[$forumid]->cache->caps = array();
 610                  unset($coursemodules[$forumid]->uservisible);
 611              }
 612  
 613              foreach ($posts as $pid => $post) {
 614                  $discussion = $discussions[$post->discussion];
 615                  $forum      = $forums[$discussion->forum];
 616                  $course     = $courses[$forum->course];
 617                  $cm         =& $coursemodules[$forum->id];
 618  
 619                  // Do some checks to see if we can bail out now.
 620  
 621                  // Only active enrolled users are in the list of subscribers.
 622                  // This does not necessarily mean that the user is subscribed to the forum or to the discussion though.
 623                  if (!isset($subscribedusers[$forum->id][$userto->id])) {
 624                      // The user does not subscribe to this forum.
 625                      continue;
 626                  }
 627  
 628                  if (!\mod_forum\subscriptions::is_subscribed($userto->id, $forum, $post->discussion, $coursemodules[$forum->id])) {
 629                      // The user does not subscribe to this forum, or to this specific discussion.
 630                      continue;
 631                  }
 632  
 633                  if ($subscriptiontime = \mod_forum\subscriptions::fetch_discussion_subscription($forum->id, $userto->id)) {
 634                      // Skip posts if the user subscribed to the discussion after it was created.
 635                      if (isset($subscriptiontime[$post->discussion]) && ($subscriptiontime[$post->discussion] > $post->created)) {
 636                          continue;
 637                      }
 638                  }
 639  
 640                  // Don't send email if the forum is Q&A and the user has not posted.
 641                  // Initial topics are still mailed.
 642                  if ($forum->type == 'qanda' && !forum_get_user_posted_time($discussion->id, $userto->id) && $pid != $discussion->firstpost) {
 643                      mtrace('Did not email ' . $userto->id.' because user has not posted in discussion');
 644                      continue;
 645                  }
 646  
 647                  // Get info about the sending user.
 648                  if (array_key_exists($post->userid, $users)) {
 649                      // We might know the user already.
 650                      $userfrom = $users[$post->userid];
 651                      if (!isset($userfrom->idnumber)) {
 652                          // Minimalised user info, fetch full record.
 653                          $userfrom = $DB->get_record('user', array('id' => $userfrom->id));
 654                          forum_cron_minimise_user_record($userfrom);
 655                      }
 656  
 657                  } else if ($userfrom = $DB->get_record('user', array('id' => $post->userid))) {
 658                      forum_cron_minimise_user_record($userfrom);
 659                      // Fetch only once if possible, we can add it to user list, it will be skipped anyway.
 660                      if ($userscount <= FORUM_CRON_USER_CACHE) {
 661                          $userscount++;
 662                          $users[$userfrom->id] = $userfrom;
 663                      }
 664                  } else {
 665                      mtrace('Could not find user ' . $post->userid . ', author of post ' . $post->id . '. Unable to send message.');
 666                      continue;
 667                  }
 668  
 669                  // Note: If we want to check that userto and userfrom are not the same person this is probably the spot to do it.
 670  
 671                  // Setup global $COURSE properly - needed for roles and languages.
 672                  cron_setup_user($userto, $course);
 673  
 674                  // Fill caches.
 675                  if (!isset($userto->viewfullnames[$forum->id])) {
 676                      $modcontext = context_module::instance($cm->id);
 677                      $userto->viewfullnames[$forum->id] = has_capability('moodle/site:viewfullnames', $modcontext);
 678                  }
 679                  if (!isset($userto->canpost[$discussion->id])) {
 680                      $modcontext = context_module::instance($cm->id);
 681                      $userto->canpost[$discussion->id] = forum_user_can_post($forum, $discussion, $userto, $cm, $course, $modcontext);
 682                  }
 683                  if (!isset($userfrom->groups[$forum->id])) {
 684                      if (!isset($userfrom->groups)) {
 685                          $userfrom->groups = array();
 686                          if (isset($users[$userfrom->id])) {
 687                              $users[$userfrom->id]->groups = array();
 688                          }
 689                      }
 690                      $userfrom->groups[$forum->id] = groups_get_all_groups($course->id, $userfrom->id, $cm->groupingid);
 691                      if (isset($users[$userfrom->id])) {
 692                          $users[$userfrom->id]->groups[$forum->id] = $userfrom->groups[$forum->id];
 693                      }
 694                  }
 695  
 696                  // Make sure groups allow this user to see this email.
 697                  if ($discussion->groupid > 0 and $groupmode = groups_get_activity_groupmode($cm, $course)) {
 698                      // Groups are being used.
 699                      if (!groups_group_exists($discussion->groupid)) {
 700                          // Can't find group - be safe and don't this message.
 701                          continue;
 702                      }
 703  
 704                      if (!groups_is_member($discussion->groupid) and !has_capability('moodle/site:accessallgroups', $modcontext)) {
 705                          // Do not send posts from other groups when in SEPARATEGROUPS or VISIBLEGROUPS.
 706                          continue;
 707                      }
 708                  }
 709  
 710                  // Make sure we're allowed to see the post.
 711                  if (!forum_user_can_see_post($forum, $discussion, $post, null, $cm)) {
 712                      mtrace('User ' . $userto->id .' can not see ' . $post->id . '. Not sending message.');
 713                      continue;
 714                  }
 715  
 716                  // OK so we need to send the email.
 717  
 718                  // Does the user want this post in a digest?  If so postpone it for now.
 719                  $maildigest = forum_get_user_maildigest_bulk($digests, $userto, $forum->id);
 720  
 721                  if ($maildigest > 0) {
 722                      // This user wants the mails to be in digest form.
 723                      $queue = new stdClass();
 724                      $queue->userid       = $userto->id;
 725                      $queue->discussionid = $discussion->id;
 726                      $queue->postid       = $post->id;
 727                      $queue->timemodified = $post->created;
 728                      $DB->insert_record('forum_queue', $queue);
 729                      continue;
 730                  }
 731  
 732                  // Prepare to actually send the post now, and build up the content.
 733  
 734                  $cleanforumname = str_replace('"', "'", strip_tags(format_string($forum->name)));
 735  
 736                  $userfrom->customheaders = array (
 737                      // Headers to make emails easier to track.
 738                      'List-Id: "'        . $cleanforumname . '" <moodleforum' . $forum->id . '@' . $hostname.'>',
 739                      'List-Help: '       . $CFG->wwwroot . '/mod/forum/view.php?f=' . $forum->id,
 740                      'Message-ID: '      . forum_get_email_message_id($post->id, $userto->id, $hostname),
 741                      'X-Course-Id: '     . $course->id,
 742                      'X-Course-Name: '   . format_string($course->fullname, true),
 743  
 744                      // Headers to help prevent auto-responders.
 745                      'Precedence: Bulk',
 746                      'X-Auto-Response-Suppress: All',
 747                      'Auto-Submitted: auto-generated',
 748                  );
 749  
 750                  if ($post->parent) {
 751                      // This post is a reply, so add headers for threading (see MDL-22551).
 752                      $userfrom->customheaders[] = 'In-Reply-To: ' . forum_get_email_message_id($post->parent, $userto->id, $hostname);
 753                      $userfrom->customheaders[] = 'References: '  . forum_get_email_message_id($post->parent, $userto->id, $hostname);
 754                  }
 755  
 756                  $shortname = format_string($course->shortname, true, array('context' => context_course::instance($course->id)));
 757  
 758                  // Generate a reply-to address from using the Inbound Message handler.
 759                  $replyaddress = null;
 760                  if ($userto->canpost[$discussion->id] && array_key_exists($post->id, $messageinboundhandlers)) {
 761                      $messageinboundgenerator->set_data($post->id, $messageinboundhandlers[$post->id]);
 762                      $replyaddress = $messageinboundgenerator->generate($userto->id);
 763                  }
 764  
 765                  $a = new stdClass();
 766                  $a->courseshortname = $shortname;
 767                  $a->forumname = $cleanforumname;
 768                  $a->subject = format_string($post->subject, true);
 769                  $postsubject = html_to_text(get_string('postmailsubject', 'forum', $a), 0);
 770                  $posttext = forum_make_mail_text($course, $cm, $forum, $discussion, $post, $userfrom, $userto, false,
 771                          $replyaddress);
 772                  $posthtml = forum_make_mail_html($course, $cm, $forum, $discussion, $post, $userfrom, $userto,
 773                          $replyaddress);
 774  
 775                  // Send the post now!
 776                  mtrace('Sending ', '');
 777  
 778                  $eventdata = new stdClass();
 779                  $eventdata->component           = 'mod_forum';
 780                  $eventdata->name                = 'posts';
 781                  $eventdata->userfrom            = $userfrom;
 782                  $eventdata->userto              = $userto;
 783                  $eventdata->subject             = $postsubject;
 784                  $eventdata->fullmessage         = $posttext;
 785                  $eventdata->fullmessageformat   = FORMAT_PLAIN;
 786                  $eventdata->fullmessagehtml     = $posthtml;
 787                  $eventdata->notification        = 1;
 788                  $eventdata->replyto             = $replyaddress;
 789  
 790                  // If forum_replytouser is not set then send mail using the noreplyaddress.
 791                  if (empty($CFG->forum_replytouser)) {
 792                      // Clone userfrom as it is referenced by $users.
 793                      $cloneduserfrom = clone($userfrom);
 794                      $cloneduserfrom->email = $CFG->noreplyaddress;
 795                      $eventdata->userfrom = $cloneduserfrom;
 796                  }
 797  
 798                  $smallmessagestrings = new stdClass();
 799                  $smallmessagestrings->user          = fullname($userfrom);
 800                  $smallmessagestrings->forumname     = "$shortname: " . format_string($forum->name, true) . ": " . $discussion->name;
 801                  $smallmessagestrings->message       = $post->message;
 802  
 803                  // Make sure strings are in message recipients language.
 804                  $eventdata->smallmessage = get_string_manager()->get_string('smallmessage', 'forum', $smallmessagestrings, $userto->lang);
 805  
 806                  $contexturl = new moodle_url('/mod/forum/discuss.php', array('d' => $discussion->id), 'p' . $post->id);
 807                  $eventdata->contexturl = $contexturl->out();
 808                  $eventdata->contexturlname = $discussion->name;
 809  
 810                  $mailresult = message_send($eventdata);
 811                  if (!$mailresult) {
 812                      mtrace("Error: mod/forum/lib.php forum_cron(): Could not send out mail for id $post->id to user $userto->id".
 813                              " ($userto->email) .. not trying again.");
 814                      $errorcount[$post->id]++;
 815                  } else {
 816                      $mailcount[$post->id]++;
 817  
 818                      // Mark post as read if forum_usermarksread is set off.
 819                      if (!$CFG->forum_usermarksread) {
 820                          $userto->markposts[$post->id] = $post->id;
 821                      }
 822                  }
 823  
 824                  mtrace('post ' . $post->id . ': ' . $post->subject);
 825              }
 826  
 827              // Mark processed posts as read.
 828              forum_tp_mark_posts_read($userto, $userto->markposts);
 829              unset($userto);
 830          }
 831      }
 832  
 833      if ($posts) {
 834          foreach ($posts as $post) {
 835              mtrace($mailcount[$post->id]." users were sent post $post->id, '$post->subject'");
 836              if ($errorcount[$post->id]) {
 837                  $DB->set_field('forum_posts', 'mailed', FORUM_MAILED_ERROR, array('id' => $post->id));
 838              }
 839          }
 840      }
 841  
 842      // release some memory
 843      unset($subscribedusers);
 844      unset($mailcount);
 845      unset($errorcount);
 846  
 847      cron_setup_user();
 848  
 849      $sitetimezone = $CFG->timezone;
 850  
 851      // Now see if there are any digest mails waiting to be sent, and if we should send them
 852  
 853      mtrace('Starting digest processing...');
 854  
 855      core_php_time_limit::raise(300); // terminate if not able to fetch all digests in 5 minutes
 856  
 857      if (!isset($CFG->digestmailtimelast)) {    // To catch the first time
 858          set_config('digestmailtimelast', 0);
 859      }
 860  
 861      $timenow = time();
 862      $digesttime = usergetmidnight($timenow, $sitetimezone) + ($CFG->digestmailtime * 3600);
 863  
 864      // Delete any really old ones (normally there shouldn't be any)
 865      $weekago = $timenow - (7 * 24 * 3600);
 866      $DB->delete_records_select('forum_queue', "timemodified < ?", array($weekago));
 867      mtrace ('Cleaned old digest records');
 868  
 869      if ($CFG->digestmailtimelast < $digesttime and $timenow > $digesttime) {
 870  
 871          mtrace('Sending forum digests: '.userdate($timenow, '', $sitetimezone));
 872  
 873          $digestposts_rs = $DB->get_recordset_select('forum_queue', "timemodified < ?", array($digesttime));
 874  
 875          if ($digestposts_rs->valid()) {
 876  
 877              // We have work to do
 878              $usermailcount = 0;
 879  
 880              //caches - reuse the those filled before too
 881              $discussionposts = array();
 882              $userdiscussions = array();
 883  
 884              foreach ($digestposts_rs as $digestpost) {
 885                  if (!isset($posts[$digestpost->postid])) {
 886                      if ($post = $DB->get_record('forum_posts', array('id' => $digestpost->postid))) {
 887                          $posts[$digestpost->postid] = $post;
 888                      } else {
 889                          continue;
 890                      }
 891                  }
 892                  $discussionid = $digestpost->discussionid;
 893                  if (!isset($discussions[$discussionid])) {
 894                      if ($discussion = $DB->get_record('forum_discussions', array('id' => $discussionid))) {
 895                          $discussions[$discussionid] = $discussion;
 896                      } else {
 897                          continue;
 898                      }
 899                  }
 900                  $forumid = $discussions[$discussionid]->forum;
 901                  if (!isset($forums[$forumid])) {
 902                      if ($forum = $DB->get_record('forum', array('id' => $forumid))) {
 903                          $forums[$forumid] = $forum;
 904                      } else {
 905                          continue;
 906                      }
 907                  }
 908  
 909                  $courseid = $forums[$forumid]->course;
 910                  if (!isset($courses[$courseid])) {
 911                      if ($course = $DB->get_record('course', array('id' => $courseid))) {
 912                          $courses[$courseid] = $course;
 913                      } else {
 914                          continue;
 915                      }
 916                  }
 917  
 918                  if (!isset($coursemodules[$forumid])) {
 919                      if ($cm = get_coursemodule_from_instance('forum', $forumid, $courseid)) {
 920                          $coursemodules[$forumid] = $cm;
 921                      } else {
 922                          continue;
 923                      }
 924                  }
 925                  $userdiscussions[$digestpost->userid][$digestpost->discussionid] = $digestpost->discussionid;
 926                  $discussionposts[$digestpost->discussionid][$digestpost->postid] = $digestpost->postid;
 927              }
 928              $digestposts_rs->close(); /// Finished iteration, let's close the resultset
 929  
 930              // Data collected, start sending out emails to each user
 931              foreach ($userdiscussions as $userid => $thesediscussions) {
 932  
 933                  core_php_time_limit::raise(120); // terminate if processing of any account takes longer than 2 minutes
 934  
 935                  cron_setup_user();
 936  
 937                  mtrace(get_string('processingdigest', 'forum', $userid), '... ');
 938  
 939                  // First of all delete all the queue entries for this user
 940                  $DB->delete_records_select('forum_queue', "userid = ? AND timemodified < ?", array($userid, $digesttime));
 941  
 942                  // Init user caches - we keep the cache for one cycle only,
 943                  // otherwise it would unnecessarily consume memory.
 944                  if (array_key_exists($userid, $users) and isset($users[$userid]->username)) {
 945                      $userto = clone($users[$userid]);
 946                  } else {
 947                      $userto = $DB->get_record('user', array('id' => $userid));
 948                      forum_cron_minimise_user_record($userto);
 949                  }
 950                  $userto->viewfullnames = array();
 951                  $userto->canpost       = array();
 952                  $userto->markposts     = array();
 953  
 954                  // Override the language and timezone of the "current" user, so that
 955                  // mail is customised for the receiver.
 956                  cron_setup_user($userto);
 957  
 958                  $postsubject = get_string('digestmailsubject', 'forum', format_string($site->shortname, true));
 959  
 960                  $headerdata = new stdClass();
 961                  $headerdata->sitename = format_string($site->fullname, true);
 962                  $headerdata->userprefs = $CFG->wwwroot.'/user/edit.php?id='.$userid.'&amp;course='.$site->id;
 963  
 964                  $posttext = get_string('digestmailheader', 'forum', $headerdata)."\n\n";
 965                  $headerdata->userprefs = '<a target="_blank" href="'.$headerdata->userprefs.'">'.get_string('digestmailprefs', 'forum').'</a>';
 966  
 967                  $posthtml = "<head>";
 968  /*                foreach ($CFG->stylesheets as $stylesheet) {
 969                      //TODO: MDL-21120
 970                      $posthtml .= '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'" />'."\n";
 971                  }*/
 972                  $posthtml .= "</head>\n<body id=\"email\">\n";
 973                  $posthtml .= '<p>'.get_string('digestmailheader', 'forum', $headerdata).'</p><br /><hr size="1" noshade="noshade" />';
 974  
 975                  foreach ($thesediscussions as $discussionid) {
 976  
 977                      core_php_time_limit::raise(120);   // to be reset for each post
 978  
 979                      $discussion = $discussions[$discussionid];
 980                      $forum      = $forums[$discussion->forum];
 981                      $course     = $courses[$forum->course];
 982                      $cm         = $coursemodules[$forum->id];
 983  
 984                      //override language
 985                      cron_setup_user($userto, $course);
 986  
 987                      // Fill caches
 988                      if (!isset($userto->viewfullnames[$forum->id])) {
 989                          $modcontext = context_module::instance($cm->id);
 990                          $userto->viewfullnames[$forum->id] = has_capability('moodle/site:viewfullnames', $modcontext);
 991                      }
 992                      if (!isset($userto->canpost[$discussion->id])) {
 993                          $modcontext = context_module::instance($cm->id);
 994                          $userto->canpost[$discussion->id] = forum_user_can_post($forum, $discussion, $userto, $cm, $course, $modcontext);
 995                      }
 996  
 997                      $strforums      = get_string('forums', 'forum');
 998                      $canunsubscribe = ! \mod_forum\subscriptions::is_forcesubscribed($forum);
 999                      $canreply       = $userto->canpost[$discussion->id];
1000                      $shortname = format_string($course->shortname, true, array('context' => context_course::instance($course->id)));
1001  
1002                      $posttext .= "\n \n";
1003                      $posttext .= '=====================================================================';
1004                      $posttext .= "\n \n";
1005                      $posttext .= "$shortname -> $strforums -> ".format_string($forum->name,true);
1006                      if ($discussion->name != $forum->name) {
1007                          $posttext  .= " -> ".format_string($discussion->name,true);
1008                      }
1009                      $posttext .= "\n";
1010                      $posttext .= $CFG->wwwroot.'/mod/forum/discuss.php?d='.$discussion->id;
1011                      $posttext .= "\n";
1012  
1013                      $posthtml .= "<p><font face=\"sans-serif\">".
1014                      "<a target=\"_blank\" href=\"$CFG->wwwroot/course/view.php?id=$course->id\">$shortname</a> -> ".
1015                      "<a target=\"_blank\" href=\"$CFG->wwwroot/mod/forum/index.php?id=$course->id\">$strforums</a> -> ".
1016                      "<a target=\"_blank\" href=\"$CFG->wwwroot/mod/forum/view.php?f=$forum->id\">".format_string($forum->name,true)."</a>";
1017                      if ($discussion->name == $forum->name) {
1018                          $posthtml .= "</font></p>";
1019                      } else {
1020                          $posthtml .= " -> <a target=\"_blank\" href=\"$CFG->wwwroot/mod/forum/discuss.php?d=$discussion->id\">".format_string($discussion->name,true)."</a></font></p>";
1021                      }
1022                      $posthtml .= '<p>';
1023  
1024                      $postsarray = $discussionposts[$discussionid];
1025                      sort($postsarray);
1026  
1027                      foreach ($postsarray as $postid) {
1028                          $post = $posts[$postid];
1029  
1030                          if (array_key_exists($post->userid, $users)) { // we might know him/her already
1031                              $userfrom = $users[$post->userid];
1032                              if (!isset($userfrom->idnumber)) {
1033                                  $userfrom = $DB->get_record('user', array('id' => $userfrom->id));
1034                                  forum_cron_minimise_user_record($userfrom);
1035                              }
1036  
1037                          } else if ($userfrom = $DB->get_record('user', array('id' => $post->userid))) {
1038                              forum_cron_minimise_user_record($userfrom);
1039                              if ($userscount <= FORUM_CRON_USER_CACHE) {
1040                                  $userscount++;
1041                                  $users[$userfrom->id] = $userfrom;
1042                              }
1043  
1044                          } else {
1045                              mtrace('Could not find user '.$post->userid);
1046                              continue;
1047                          }
1048  
1049                          if (!isset($userfrom->groups[$forum->id])) {
1050                              if (!isset($userfrom->groups)) {
1051                                  $userfrom->groups = array();
1052                                  if (isset($users[$userfrom->id])) {
1053                                      $users[$userfrom->id]->groups = array();
1054                                  }
1055                              }
1056                              $userfrom->groups[$forum->id] = groups_get_all_groups($course->id, $userfrom->id, $cm->groupingid);
1057                              if (isset($users[$userfrom->id])) {
1058                                  $users[$userfrom->id]->groups[$forum->id] = $userfrom->groups[$forum->id];
1059                              }
1060                          }
1061  
1062                          // Headers to help prevent auto-responders.
1063                          $userfrom->customheaders = array(
1064                                  "Precedence: Bulk",
1065                                  'X-Auto-Response-Suppress: All',
1066                                  'Auto-Submitted: auto-generated',
1067                              );
1068  
1069                          $maildigest = forum_get_user_maildigest_bulk($digests, $userto, $forum->id);
1070                          if ($maildigest == 2) {
1071                              // Subjects and link only
1072                              $posttext .= "\n";
1073                              $posttext .= $CFG->wwwroot.'/mod/forum/discuss.php?d='.$discussion->id;
1074                              $by = new stdClass();
1075                              $by->name = fullname($userfrom);
1076                              $by->date = userdate($post->modified);
1077                              $posttext .= "\n".format_string($post->subject,true).' '.get_string("bynameondate", "forum", $by);
1078                              $posttext .= "\n---------------------------------------------------------------------";
1079  
1080                              $by->name = "<a target=\"_blank\" href=\"$CFG->wwwroot/user/view.php?id=$userfrom->id&amp;course=$course->id\">$by->name</a>";
1081                              $posthtml .= '<div><a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$discussion->id.'#p'.$post->id.'">'.format_string($post->subject,true).'</a> '.get_string("bynameondate", "forum", $by).'</div>';
1082  
1083                          } else {
1084                              // The full treatment
1085                              $posttext .= forum_make_mail_text($course, $cm, $forum, $discussion, $post, $userfrom, $userto, true);
1086                              $posthtml .= forum_make_mail_post($course, $cm, $forum, $discussion, $post, $userfrom, $userto, false, $canreply, true, false);
1087  
1088                          // Create an array of postid's for this user to mark as read.
1089                              if (!$CFG->forum_usermarksread) {
1090                                  $userto->markposts[$post->id] = $post->id;
1091                              }
1092                          }
1093                      }
1094                      $footerlinks = array();
1095                      if ($canunsubscribe) {
1096                          $footerlinks[] = "<a href=\"$CFG->wwwroot/mod/forum/subscribe.php?id=$forum->id\">" . get_string("unsubscribe", "forum") . "</a>";
1097                      } else {
1098                          $footerlinks[] = get_string("everyoneissubscribed", "forum");
1099                      }
1100                      $footerlinks[] = "<a href='{$CFG->wwwroot}/mod/forum/index.php?id={$forum->course}'>" . get_string("digestmailpost", "forum") . '</a>';
1101                      $posthtml .= "\n<div class='mdl-right'><font size=\"1\">" . implode('&nbsp;', $footerlinks) . '</font></div>';
1102                      $posthtml .= '<hr size="1" noshade="noshade" /></p>';
1103                  }
1104                  $posthtml .= '</body>';
1105  
1106                  if (empty($userto->mailformat) || $userto->mailformat != 1) {
1107                      // This user DOESN'T want to receive HTML
1108                      $posthtml = '';
1109                  }
1110  
1111                  $attachment = $attachname='';
1112                  // Directly email forum digests rather than sending them via messaging, use the
1113                  // site shortname as 'from name', the noreply address will be used by email_to_user.
1114                  $mailresult = email_to_user($userto, $site->shortname, $postsubject, $posttext, $posthtml, $attachment, $attachname);
1115  
1116                  if (!$mailresult) {
1117                      mtrace("ERROR: mod/forum/cron.php: Could not send out digest mail to user $userto->id ".
1118                          "($userto->email)... not trying again.");
1119                  } else {
1120                      mtrace("success.");
1121                      $usermailcount++;
1122  
1123                      // Mark post as read if forum_usermarksread is set off
1124                      forum_tp_mark_posts_read($userto, $userto->markposts);
1125                  }
1126              }
1127          }
1128      /// We have finishied all digest emails, update $CFG->digestmailtimelast
1129          set_config('digestmailtimelast', $timenow);
1130      }
1131  
1132      cron_setup_user();
1133  
1134      if (!empty($usermailcount)) {
1135          mtrace(get_string('digestsentusers', 'forum', $usermailcount));
1136      }
1137  
1138      if (!empty($CFG->forum_lastreadclean)) {
1139          $timenow = time();
1140          if ($CFG->forum_lastreadclean + (24*3600) < $timenow) {
1141              set_config('forum_lastreadclean', $timenow);
1142              mtrace('Removing old forum read tracking info...');
1143              forum_tp_clean_read_records();
1144          }
1145      } else {
1146          set_config('forum_lastreadclean', time());
1147      }
1148  
1149      return true;
1150  }
1151  
1152  /**
1153   * Builds and returns the body of the email notification in plain text.
1154   *
1155   * @global object
1156   * @global object
1157   * @uses CONTEXT_MODULE
1158   * @param object $course
1159   * @param object $cm
1160   * @param object $forum
1161   * @param object $discussion
1162   * @param object $post
1163   * @param object $userfrom
1164   * @param object $userto
1165   * @param boolean $bare
1166   * @param string $replyaddress The inbound address that a user can reply to the generated e-mail with. [Since 2.8].
1167   * @return string The email body in plain text format.
1168   */
1169  function forum_make_mail_text($course, $cm, $forum, $discussion, $post, $userfrom, $userto, $bare = false, $replyaddress = null) {
1170      global $CFG, $USER;
1171  
1172      $modcontext = context_module::instance($cm->id);
1173  
1174      if (!isset($userto->viewfullnames[$forum->id])) {
1175          $viewfullnames = has_capability('moodle/site:viewfullnames', $modcontext, $userto->id);
1176      } else {
1177          $viewfullnames = $userto->viewfullnames[$forum->id];
1178      }
1179  
1180      if (!isset($userto->canpost[$discussion->id])) {
1181          $canreply = forum_user_can_post($forum, $discussion, $userto, $cm, $course, $modcontext);
1182      } else {
1183          $canreply = $userto->canpost[$discussion->id];
1184      }
1185  
1186      $by = New stdClass;
1187      $by->name = fullname($userfrom, $viewfullnames);
1188      $by->date = userdate($post->modified, "", $userto->timezone);
1189  
1190      $strbynameondate = get_string('bynameondate', 'forum', $by);
1191  
1192      $strforums = get_string('forums', 'forum');
1193  
1194      $canunsubscribe = !\mod_forum\subscriptions::is_forcesubscribed($forum);
1195  
1196      $posttext = '';
1197  
1198      if (!$bare) {
1199          $shortname = format_string($course->shortname, true, array('context' => context_course::instance($course->id)));
1200          $posttext  .= "$shortname -> $strforums -> ".format_string($forum->name,true);
1201  
1202          if ($discussion->name != $forum->name) {
1203              $posttext  .= " -> ".format_string($discussion->name,true);
1204          }
1205      }
1206  
1207      // add absolute file links
1208      $post->message = file_rewrite_pluginfile_urls($post->message, 'pluginfile.php', $modcontext->id, 'mod_forum', 'post', $post->id);
1209  
1210      $posttext .= "\n";
1211      $posttext .= $CFG->wwwroot.'/mod/forum/discuss.php?d='.$discussion->id;
1212      $posttext .= "\n---------------------------------------------------------------------\n";
1213      $posttext .= format_string($post->subject,true);
1214      if ($bare) {
1215          $posttext .= " ($CFG->wwwroot/mod/forum/discuss.php?d=$discussion->id#p$post->id)";
1216      }
1217      $posttext .= "\n".$strbynameondate."\n";
1218      $posttext .= "---------------------------------------------------------------------\n";
1219      $posttext .= format_text_email($post->message, $post->messageformat);
1220      $posttext .= "\n\n";
1221      $posttext .= forum_print_attachments($post, $cm, "text");
1222  
1223      if (!$bare) {
1224          if ($canreply) {
1225              $posttext .= "---------------------------------------------------------------------\n";
1226              $posttext .= get_string("postmailinfo", "forum", $shortname)."\n";
1227              $posttext .= "$CFG->wwwroot/mod/forum/post.php?reply=$post->id\n";
1228          }
1229  
1230          if ($canunsubscribe) {
1231              if (\mod_forum\subscriptions::is_subscribed($userto->id, $forum, null, $cm)) {
1232                  // If subscribed to this forum, offer the unsubscribe link.
1233                  $posttext .= "\n---------------------------------------------------------------------\n";
1234                  $posttext .= get_string("unsubscribe", "forum");
1235                  $posttext .= ": $CFG->wwwroot/mod/forum/subscribe.php?id=$forum->id\n";
1236              }
1237              // Always offer the unsubscribe from discussion link.
1238              $posttext .= "\n---------------------------------------------------------------------\n";
1239              $posttext .= get_string("unsubscribediscussion", "forum");
1240              $posttext .= ": $CFG->wwwroot/mod/forum/subscribe.php?id=$forum->id&d=$discussion->id\n";
1241          }
1242      }
1243  
1244      $posttext .= "\n---------------------------------------------------------------------\n";
1245      $posttext .= get_string("digestmailpost", "forum");
1246      $posttext .= ": {$CFG->wwwroot}/mod/forum/index.php?id={$forum->course}\n";
1247  
1248      if ($replyaddress) {
1249          $posttext .= "\n\n" . get_string('replytopostbyemail', 'mod_forum');
1250      }
1251  
1252      return $posttext;
1253  }
1254  
1255  /**
1256   * Builds and returns the body of the email notification in html format.
1257   *
1258   * @global object
1259   * @param object $course
1260   * @param object $cm
1261   * @param object $forum
1262   * @param object $discussion
1263   * @param object $post
1264   * @param object $userfrom
1265   * @param object $userto
1266   * @param string $replyaddress The inbound address that a user can reply to the generated e-mail with. [Since 2.8].
1267   * @return string The email text in HTML format
1268   */
1269  function forum_make_mail_html($course, $cm, $forum, $discussion, $post, $userfrom, $userto, $replyaddress = null) {
1270      global $CFG;
1271  
1272      if ($userto->mailformat != 1) {  // Needs to be HTML
1273          return '';
1274      }
1275  
1276      if (!isset($userto->canpost[$discussion->id])) {
1277          $canreply = forum_user_can_post($forum, $discussion, $userto, $cm, $course);
1278      } else {
1279          $canreply = $userto->canpost[$discussion->id];
1280      }
1281  
1282      $strforums = get_string('forums', 'forum');
1283      $canunsubscribe = ! \mod_forum\subscriptions::is_forcesubscribed($forum);
1284      $shortname = format_string($course->shortname, true, array('context' => context_course::instance($course->id)));
1285  
1286      $posthtml = '<head>';
1287  /*    foreach ($CFG->stylesheets as $stylesheet) {
1288          //TODO: MDL-21120
1289          $posthtml .= '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'" />'."\n";
1290      }*/
1291      $posthtml .= '</head>';
1292      $posthtml .= "\n<body id=\"email\">\n\n";
1293  
1294      $posthtml .= '<div class="navbar">'.
1295      '<a target="_blank" href="'.$CFG->wwwroot.'/course/view.php?id='.$course->id.'">'.$shortname.'</a> &raquo; '.
1296      '<a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/index.php?id='.$course->id.'">'.$strforums.'</a> &raquo; '.
1297      '<a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/view.php?f='.$forum->id.'">'.format_string($forum->name,true).'</a>';
1298      if ($discussion->name == $forum->name) {
1299          $posthtml .= '</div>';
1300      } else {
1301          $posthtml .= ' &raquo; <a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$discussion->id.'">'.
1302                       format_string($discussion->name,true).'</a></div>';
1303      }
1304      $posthtml .= forum_make_mail_post($course, $cm, $forum, $discussion, $post, $userfrom, $userto, false, $canreply, true, false);
1305  
1306      if ($replyaddress) {
1307          $posthtml .= html_writer::tag('p', get_string('replytopostbyemail', 'mod_forum'));
1308      }
1309  
1310      $footerlinks = array();
1311      if ($canunsubscribe) {
1312          if (\mod_forum\subscriptions::is_subscribed($userto->id, $forum, null, $cm)) {
1313              // If subscribed to this forum, offer the unsubscribe link.
1314              $unsublink = new moodle_url('/mod/forum/subscribe.php', array('id' => $forum->id));
1315              $footerlinks[] = html_writer::link($unsublink, get_string('unsubscribe', 'mod_forum'));
1316          }
1317          // Always offer the unsubscribe from discussion link.
1318          $unsublink = new moodle_url('/mod/forum/subscribe.php', array(
1319                  'id' => $forum->id,
1320                  'd' => $discussion->id,
1321              ));
1322          $footerlinks[] = html_writer::link($unsublink, get_string('unsubscribediscussion', 'mod_forum'));
1323  
1324          $footerlinks[] = '<a href="' . $CFG->wwwroot . '/mod/forum/unsubscribeall.php">' . get_string('unsubscribeall', 'forum') . '</a>';
1325      }
1326      $footerlinks[] = "<a href='{$CFG->wwwroot}/mod/forum/index.php?id={$forum->course}'>" . get_string('digestmailpost', 'forum') . '</a>';
1327      $posthtml .= '<hr /><div class="mdl-align unsubscribelink">' . implode('&nbsp;', $footerlinks) . '</div>';
1328  
1329      $posthtml .= '</body>';
1330  
1331      return $posthtml;
1332  }
1333  
1334  
1335  /**
1336   *
1337   * @param object $course
1338   * @param object $user
1339   * @param object $mod TODO this is not used in this function, refactor
1340   * @param object $forum
1341   * @return object A standard object with 2 variables: info (number of posts for this user) and time (last modified)
1342   */
1343  function forum_user_outline($course, $user, $mod, $forum) {
1344      global $CFG;
1345      require_once("$CFG->libdir/gradelib.php");
1346      $grades = grade_get_grades($course->id, 'mod', 'forum', $forum->id, $user->id);
1347      if (empty($grades->items[0]->grades)) {
1348          $grade = false;
1349      } else {
1350          $grade = reset($grades->items[0]->grades);
1351      }
1352  
1353      $count = forum_count_user_posts($forum->id, $user->id);
1354  
1355      if ($count && $count->postcount > 0) {
1356          $result = new stdClass();
1357          $result->info = get_string("numposts", "forum", $count->postcount);
1358          $result->time = $count->lastpost;
1359          if ($grade) {
1360              $result->info .= ', ' . get_string('grade') . ': ' . $grade->str_long_grade;
1361          }
1362          return $result;
1363      } else if ($grade) {
1364          $result = new stdClass();
1365          $result->info = get_string('grade') . ': ' . $grade->str_long_grade;
1366  
1367          //datesubmitted == time created. dategraded == time modified or time overridden
1368          //if grade was last modified by the user themselves use date graded. Otherwise use date submitted
1369          //TODO: move this copied & pasted code somewhere in the grades API. See MDL-26704
1370          if ($grade->usermodified == $user->id || empty($grade->datesubmitted)) {
1371              $result->time = $grade->dategraded;
1372          } else {
1373              $result->time = $grade->datesubmitted;
1374          }
1375  
1376          return $result;
1377      }
1378      return NULL;
1379  }
1380  
1381  
1382  /**
1383   * @global object
1384   * @global object
1385   * @param object $coure
1386   * @param object $user
1387   * @param object $mod
1388   * @param object $forum
1389   */
1390  function forum_user_complete($course, $user, $mod, $forum) {
1391      global $CFG,$USER, $OUTPUT;
1392      require_once("$CFG->libdir/gradelib.php");
1393  
1394      $grades = grade_get_grades($course->id, 'mod', 'forum', $forum->id, $user->id);
1395      if (!empty($grades->items[0]->grades)) {
1396          $grade = reset($grades->items[0]->grades);
1397          echo $OUTPUT->container(get_string('grade').': '.$grade->str_long_grade);
1398          if ($grade->str_feedback) {
1399              echo $OUTPUT->container(get_string('feedback').': '.$grade->str_feedback);
1400          }
1401      }
1402  
1403      if ($posts = forum_get_user_posts($forum->id, $user->id)) {
1404  
1405          if (!$cm = get_coursemodule_from_instance('forum', $forum->id, $course->id)) {
1406              print_error('invalidcoursemodule');
1407          }
1408          $discussions = forum_get_user_involved_discussions($forum->id, $user->id);
1409  
1410          foreach ($posts as $post) {
1411              if (!isset($discussions[$post->discussion])) {
1412                  continue;
1413              }
1414              $discussion = $discussions[$post->discussion];
1415  
1416              forum_print_post($post, $discussion, $forum, $cm, $course, false, false, false);
1417          }
1418      } else {
1419          echo "<p>".get_string("noposts", "forum")."</p>";
1420      }
1421  }
1422  
1423  /**
1424   * Filters the forum discussions according to groups membership and config.
1425   *
1426   * @since  Moodle 2.8, 2.7.1, 2.6.4
1427   * @param  array $discussions Discussions with new posts array
1428   * @return array Forums with the number of new posts
1429   */
1430  function forum_filter_user_groups_discussions($discussions) {
1431  
1432      // Group the remaining discussions posts by their forumid.
1433      $filteredforums = array();
1434  
1435      // Discard not visible groups.
1436      foreach ($discussions as $discussion) {
1437  
1438          // Course data is already cached.
1439          $instances = get_fast_modinfo($discussion->course)->get_instances();
1440          $forum = $instances['forum'][$discussion->forum];
1441  
1442          // Continue if the user should not see this discussion.
1443          if (!forum_is_user_group_discussion($forum, $discussion->groupid)) {
1444              continue;
1445          }
1446  
1447          // Grouping results by forum.
1448          if (empty($filteredforums[$forum->instance])) {
1449              $filteredforums[$forum->instance] = new stdClass();
1450              $filteredforums[$forum->instance]->id = $forum->id;
1451              $filteredforums[$forum->instance]->count = 0;
1452          }
1453          $filteredforums[$forum->instance]->count += $discussion->count;
1454  
1455      }
1456  
1457      return $filteredforums;
1458  }
1459  
1460  /**
1461   * Returns whether the discussion group is visible by the current user or not.
1462   *
1463   * @since Moodle 2.8, 2.7.1, 2.6.4
1464   * @param cm_info $cm The discussion course module
1465   * @param int $discussiongroupid The discussion groupid
1466   * @return bool
1467   */
1468  function forum_is_user_group_discussion(cm_info $cm, $discussiongroupid) {
1469  
1470      if ($discussiongroupid == -1 || $cm->effectivegroupmode != SEPARATEGROUPS) {
1471          return true;
1472      }
1473  
1474      if (isguestuser()) {
1475          return false;
1476      }
1477  
1478      if (has_capability('moodle/site:accessallgroups', context_module::instance($cm->id)) ||
1479              in_array($discussiongroupid, $cm->get_modinfo()->get_groups($cm->groupingid))) {
1480          return true;
1481      }
1482  
1483      return false;
1484  }
1485  
1486  /**
1487   * @global object
1488   * @global object
1489   * @global object
1490   * @param array $courses
1491   * @param array $htmlarray
1492   */
1493  function forum_print_overview($courses,&$htmlarray) {
1494      global $USER, $CFG, $DB, $SESSION;
1495  
1496      if (empty($courses) || !is_array($courses) || count($courses) == 0) {
1497          return array();
1498      }
1499  
1500      if (!$forums = get_all_instances_in_courses('forum',$courses)) {
1501          return;
1502      }
1503  
1504      // Courses to search for new posts
1505      $coursessqls = array();
1506      $params = array();
1507      foreach ($courses as $course) {
1508  
1509          // If the user has never entered into the course all posts are pending
1510          if ($course->lastaccess == 0) {
1511              $coursessqls[] = '(d.course = ?)';
1512              $params[] = $course->id;
1513  
1514          // Only posts created after the course last access
1515          } else {
1516              $coursessqls[] = '(d.course = ? AND p.created > ?)';
1517              $params[] = $course->id;
1518              $params[] = $course->lastaccess;
1519          }
1520      }
1521      $params[] = $USER->id;
1522      $coursessql = implode(' OR ', $coursessqls);
1523  
1524      $sql = "SELECT d.id, d.forum, d.course, d.groupid, COUNT(*) as count "
1525                  .'FROM {forum_discussions} d '
1526                  .'JOIN {forum_posts} p ON p.discussion = d.id '
1527                  ."WHERE ($coursessql) "
1528                  .'AND p.userid != ? '
1529                  .'GROUP BY d.id, d.forum, d.course, d.groupid '
1530                  .'ORDER BY d.course, d.forum';
1531  
1532      // Avoid warnings.
1533      if (!$discussions = $DB->get_records_sql($sql, $params)) {
1534          $discussions = array();
1535      }
1536  
1537      $forumsnewposts = forum_filter_user_groups_discussions($discussions);
1538  
1539      // also get all forum tracking stuff ONCE.
1540      $trackingforums = array();
1541      foreach ($forums as $forum) {
1542          if (forum_tp_can_track_forums($forum)) {
1543              $trackingforums[$forum->id] = $forum;
1544          }
1545      }
1546  
1547      if (count($trackingforums) > 0) {
1548          $cutoffdate = isset($CFG->forum_oldpostdays) ? (time() - ($CFG->forum_oldpostdays*24*60*60)) : 0;
1549          $sql = 'SELECT d.forum,d.course,COUNT(p.id) AS count '.
1550              ' FROM {forum_posts} p '.
1551              ' JOIN {forum_discussions} d ON p.discussion = d.id '.
1552              ' LEFT JOIN {forum_read} r ON r.postid = p.id AND r.userid = ? WHERE (';
1553          $params = array($USER->id);
1554  
1555          foreach ($trackingforums as $track) {
1556              $sql .= '(d.forum = ? AND (d.groupid = -1 OR d.groupid = 0 OR d.groupid = ?)) OR ';
1557              $params[] = $track->id;
1558              if (isset($SESSION->currentgroup[$track->course])) {
1559                  $groupid =  $SESSION->currentgroup[$track->course];
1560              } else {
1561                  // get first groupid
1562                  $groupids = groups_get_all_groups($track->course, $USER->id);
1563                  if ($groupids) {
1564                      reset($groupids);
1565                      $groupid = key($groupids);
1566                      $SESSION->currentgroup[$track->course] = $groupid;
1567                  } else {
1568                      $groupid = 0;
1569                  }
1570                  unset($groupids);
1571              }
1572              $params[] = $groupid;
1573          }
1574          $sql = substr($sql,0,-3); // take off the last OR
1575          $sql .= ') AND p.modified >= ? AND r.id is NULL GROUP BY d.forum,d.course';
1576          $params[] = $cutoffdate;
1577  
1578          if (!$unread = $DB->get_records_sql($sql, $params)) {
1579              $unread = array();
1580          }
1581      } else {
1582          $unread = array();
1583      }
1584  
1585      if (empty($unread) and empty($forumsnewposts)) {
1586          return;
1587      }
1588  
1589      $strforum = get_string('modulename','forum');
1590  
1591      foreach ($forums as $forum) {
1592          $str = '';
1593          $count = 0;
1594          $thisunread = 0;
1595          $showunread = false;
1596          // either we have something from logs, or trackposts, or nothing.
1597          if (array_key_exists($forum->id, $forumsnewposts) && !empty($forumsnewposts[$forum->id])) {
1598              $count = $forumsnewposts[$forum->id]->count;
1599          }
1600          if (array_key_exists($forum->id,$unread)) {
1601              $thisunread = $unread[$forum->id]->count;
1602              $showunread = true;
1603          }
1604          if ($count > 0 || $thisunread > 0) {
1605              $str .= '<div class="overview forum"><div class="name">'.$strforum.': <a title="'.$strforum.'" href="'.$CFG->wwwroot.'/mod/forum/view.php?f='.$forum->id.'">'.
1606                  $forum->name.'</a></div>';
1607              $str .= '<div class="info"><span class="postsincelogin">';
1608              $str .= get_string('overviewnumpostssince', 'forum', $count)."</span>";
1609              if (!empty($showunread)) {
1610                  $str .= '<div class="unreadposts">'.get_string('overviewnumunread', 'forum', $thisunread).'</div>';
1611              }
1612              $str .= '</div></div>';
1613          }
1614          if (!empty($str)) {
1615              if (!array_key_exists($forum->course,$htmlarray)) {
1616                  $htmlarray[$forum->course] = array();
1617              }
1618              if (!array_key_exists('forum',$htmlarray[$forum->course])) {
1619                  $htmlarray[$forum->course]['forum'] = ''; // initialize, avoid warnings
1620              }
1621              $htmlarray[$forum->course]['forum'] .= $str;
1622          }
1623      }
1624  }
1625  
1626  /**
1627   * Given a course and a date, prints a summary of all the new
1628   * messages posted in the course since that date
1629   *
1630   * @global object
1631   * @global object
1632   * @global object
1633   * @uses CONTEXT_MODULE
1634   * @uses VISIBLEGROUPS
1635   * @param object $course
1636   * @param bool $viewfullnames capability
1637   * @param int $timestart
1638   * @return bool success
1639   */
1640  function forum_print_recent_activity($course, $viewfullnames, $timestart) {
1641      global $CFG, $USER, $DB, $OUTPUT;
1642  
1643      // do not use log table if possible, it may be huge and is expensive to join with other tables
1644  
1645      $allnamefields = user_picture::fields('u', null, 'duserid');
1646      if (!$posts = $DB->get_records_sql("SELECT p.*, f.type AS forumtype, d.forum, d.groupid,
1647                                                d.timestart, d.timeend, $allnamefields
1648                                           FROM {forum_posts} p
1649                                                JOIN {forum_discussions} d ON d.id = p.discussion
1650                                                JOIN {forum} f             ON f.id = d.forum
1651                                                JOIN {user} u              ON u.id = p.userid
1652                                          WHERE p.created > ? AND f.course = ?
1653                                       ORDER BY p.id ASC", array($timestart, $course->id))) { // order by initial posting date
1654           return false;
1655      }
1656  
1657      $modinfo = get_fast_modinfo($course);
1658  
1659      $groupmodes = array();
1660      $cms    = array();
1661  
1662      $strftimerecent = get_string('strftimerecent');
1663  
1664      $printposts = array();
1665      foreach ($posts as $post) {
1666          if (!isset($modinfo->instances['forum'][$post->forum])) {
1667              // not visible
1668              continue;
1669          }
1670          $cm = $modinfo->instances['forum'][$post->forum];
1671          if (!$cm->uservisible) {
1672              continue;
1673          }
1674          $context = context_module::instance($cm->id);
1675  
1676          if (!has_capability('mod/forum:viewdiscussion', $context)) {
1677              continue;
1678          }
1679  
1680          if (!empty($CFG->forum_enabletimedposts) and $USER->id != $post->duserid
1681            and (($post->timestart > 0 and $post->timestart > time()) or ($post->timeend > 0 and $post->timeend < time()))) {
1682              if (!has_capability('mod/forum:viewhiddentimedposts', $context)) {
1683                  continue;
1684              }
1685          }
1686  
1687          // Check that the user can see the discussion.
1688          if (forum_is_user_group_discussion($cm, $post->groupid)) {
1689              $printposts[] = $post;
1690          }
1691  
1692      }
1693      unset($posts);
1694  
1695      if (!$printposts) {
1696          return false;
1697      }
1698  
1699      echo $OUTPUT->heading(get_string('newforumposts', 'forum').':', 3);
1700      echo "\n<ul class='unlist'>\n";
1701  
1702      foreach ($printposts as $post) {
1703          $subjectclass = empty($post->parent) ? ' bold' : '';
1704  
1705          echo '<li><div class="head">'.
1706                 '<div class="date">'.userdate($post->modified, $strftimerecent).'</div>'.
1707                 '<div class="name">'.fullname($post, $viewfullnames).'</div>'.
1708               '</div>';
1709          echo '<div class="info'.$subjectclass.'">';
1710          if (empty($post->parent)) {
1711              echo '"<a href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$post->discussion.'">';
1712          } else {
1713              echo '"<a href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$post->discussion.'&amp;parent='.$post->parent.'#p'.$post->id.'">';
1714          }
1715          $post->subject = break_up_long_words(format_string($post->subject, true));
1716          echo $post->subject;
1717          echo "</a>\"</div></li>\n";
1718      }
1719  
1720      echo "</ul>\n";
1721  
1722      return true;
1723  }
1724  
1725  /**
1726   * Return grade for given user or all users.
1727   *
1728   * @global object
1729   * @global object
1730   * @param object $forum
1731   * @param int $userid optional user id, 0 means all users
1732   * @return array array of grades, false if none
1733   */
1734  function forum_get_user_grades($forum, $userid = 0) {
1735      global $CFG;
1736  
1737      require_once($CFG->dirroot.'/rating/lib.php');
1738  
1739      $ratingoptions = new stdClass;
1740      $ratingoptions->component = 'mod_forum';
1741      $ratingoptions->ratingarea = 'post';
1742  
1743      //need these to work backwards to get a context id. Is there a better way to get contextid from a module instance?
1744      $ratingoptions->modulename = 'forum';
1745      $ratingoptions->moduleid   = $forum->id;
1746      $ratingoptions->userid = $userid;
1747      $ratingoptions->aggregationmethod = $forum->assessed;
1748      $ratingoptions->scaleid = $forum->scale;
1749      $ratingoptions->itemtable = 'forum_posts';
1750      $ratingoptions->itemtableusercolumn = 'userid';
1751  
1752      $rm = new rating_manager();
1753      return $rm->get_user_grades($ratingoptions);
1754  }
1755  
1756  /**
1757   * Update activity grades
1758   *
1759   * @category grade
1760   * @param object $forum
1761   * @param int $userid specific user only, 0 means all
1762   * @param boolean $nullifnone return null if grade does not exist
1763   * @return void
1764   */
1765  function forum_update_grades($forum, $userid=0, $nullifnone=true) {
1766      global $CFG, $DB;
1767      require_once($CFG->libdir.'/gradelib.php');
1768  
1769      if (!$forum->assessed) {
1770          forum_grade_item_update($forum);
1771  
1772      } else if ($grades = forum_get_user_grades($forum, $userid)) {
1773          forum_grade_item_update($forum, $grades);
1774  
1775      } else if ($userid and $nullifnone) {
1776          $grade = new stdClass();
1777          $grade->userid   = $userid;
1778          $grade->rawgrade = NULL;
1779          forum_grade_item_update($forum, $grade);
1780  
1781      } else {
1782          forum_grade_item_update($forum);
1783      }
1784  }
1785  
1786  /**
1787   * Create/update grade item for given forum
1788   *
1789   * @category grade
1790   * @uses GRADE_TYPE_NONE
1791   * @uses GRADE_TYPE_VALUE
1792   * @uses GRADE_TYPE_SCALE
1793   * @param stdClass $forum Forum object with extra cmidnumber
1794   * @param mixed $grades Optional array/object of grade(s); 'reset' means reset grades in gradebook
1795   * @return int 0 if ok
1796   */
1797  function forum_grade_item_update($forum, $grades=NULL) {
1798      global $CFG;
1799      if (!function_exists('grade_update')) { //workaround for buggy PHP versions
1800          require_once($CFG->libdir.'/gradelib.php');
1801      }
1802  
1803      $params = array('itemname'=>$forum->name, 'idnumber'=>$forum->cmidnumber);
1804  
1805      if (!$forum->assessed or $forum->scale == 0) {
1806          $params['gradetype'] = GRADE_TYPE_NONE;
1807  
1808      } else if ($forum->scale > 0) {
1809          $params['gradetype'] = GRADE_TYPE_VALUE;
1810          $params['grademax']  = $forum->scale;
1811          $params['grademin']  = 0;
1812  
1813      } else if ($forum->scale < 0) {
1814          $params['gradetype'] = GRADE_TYPE_SCALE;
1815          $params['scaleid']   = -$forum->scale;
1816      }
1817  
1818      if ($grades  === 'reset') {
1819          $params['reset'] = true;
1820          $grades = NULL;
1821      }
1822  
1823      return grade_update('mod/forum', $forum->course, 'mod', 'forum', $forum->id, 0, $grades, $params);
1824  }
1825  
1826  /**
1827   * Delete grade item for given forum
1828   *
1829   * @category grade
1830   * @param stdClass $forum Forum object
1831   * @return grade_item
1832   */
1833  function forum_grade_item_delete($forum) {
1834      global $CFG;
1835      require_once($CFG->libdir.'/gradelib.php');
1836  
1837      return grade_update('mod/forum', $forum->course, 'mod', 'forum', $forum->id, 0, NULL, array('deleted'=>1));
1838  }
1839  
1840  
1841  /**
1842   * This function returns if a scale is being used by one forum
1843   *
1844   * @global object
1845   * @param int $forumid
1846   * @param int $scaleid negative number
1847   * @return bool
1848   */
1849  function forum_scale_used ($forumid,$scaleid) {
1850      global $DB;
1851      $return = false;
1852  
1853      $rec = $DB->get_record("forum",array("id" => "$forumid","scale" => "-$scaleid"));
1854  
1855      if (!empty($rec) && !empty($scaleid)) {
1856          $return = true;
1857      }
1858  
1859      return $return;
1860  }
1861  
1862  /**
1863   * Checks if scale is being used by any instance of forum
1864   *
1865   * This is used to find out if scale used anywhere
1866   *
1867   * @global object
1868   * @param $scaleid int
1869   * @return boolean True if the scale is used by any forum
1870   */
1871  function forum_scale_used_anywhere($scaleid) {
1872      global $DB;
1873      if ($scaleid and $DB->record_exists('forum', array('scale' => -$scaleid))) {
1874          return true;
1875      } else {
1876          return false;
1877      }
1878  }
1879  
1880  // SQL FUNCTIONS ///////////////////////////////////////////////////////////
1881  
1882  /**
1883   * Gets a post with all info ready for forum_print_post
1884   * Most of these joins are just to get the forum id
1885   *
1886   * @global object
1887   * @global object
1888   * @param int $postid
1889   * @return mixed array of posts or false
1890   */
1891  function forum_get_post_full($postid) {
1892      global $CFG, $DB;
1893  
1894      $allnames = get_all_user_name_fields(true, 'u');
1895      return $DB->get_record_sql("SELECT p.*, d.forum, $allnames, u.email, u.picture, u.imagealt
1896                               FROM {forum_posts} p
1897                                    JOIN {forum_discussions} d ON p.discussion = d.id
1898                                    LEFT JOIN {user} u ON p.userid = u.id
1899                              WHERE p.id = ?", array($postid));
1900  }
1901  
1902  /**
1903   * Gets all posts in discussion including top parent.
1904   *
1905   * @global object
1906   * @global object
1907   * @global object
1908   * @param int $discussionid
1909   * @param string $sort
1910   * @param bool $tracking does user track the forum?
1911   * @return array of posts
1912   */
1913  function forum_get_all_discussion_posts($discussionid, $sort, $tracking=false) {
1914      global $CFG, $DB, $USER;
1915  
1916      $tr_sel  = "";
1917      $tr_join = "";
1918      $params = array();
1919  
1920      if ($tracking) {
1921          $now = time();
1922          $cutoffdate = $now - ($CFG->forum_oldpostdays * 24 * 3600);
1923          $tr_sel  = ", fr.id AS postread";
1924          $tr_join = "LEFT JOIN {forum_read} fr ON (fr.postid = p.id AND fr.userid = ?)";
1925          $params[] = $USER->id;
1926      }
1927  
1928      $allnames = get_all_user_name_fields(true, 'u');
1929      $params[] = $discussionid;
1930      if (!$posts = $DB->get_records_sql("SELECT p.*, $allnames, u.email, u.picture, u.imagealt $tr_sel
1931                                       FROM {forum_posts} p
1932                                            LEFT JOIN {user} u ON p.userid = u.id
1933                                            $tr_join
1934                                      WHERE p.discussion = ?
1935                                   ORDER BY $sort", $params)) {
1936          return array();
1937      }
1938  
1939      foreach ($posts as $pid=>$p) {
1940          if ($tracking) {
1941              if (forum_tp_is_post_old($p)) {
1942                   $posts[$pid]->postread = true;
1943              }
1944          }
1945          if (!$p->parent) {
1946              continue;
1947          }
1948          if (!isset($posts[$p->parent])) {
1949              continue; // parent does not exist??
1950          }
1951          if (!isset($posts[$p->parent]->children)) {
1952              $posts[$p->parent]->children = array();
1953          }
1954          $posts[$p->parent]->children[$pid] =& $posts[$pid];
1955      }
1956  
1957      return $posts;
1958  }
1959  
1960  /**
1961   * An array of forum objects that the user is allowed to read/search through.
1962   *
1963   * @global object
1964   * @global object
1965   * @global object
1966   * @param int $userid
1967   * @param int $courseid if 0, we look for forums throughout the whole site.
1968   * @return array of forum objects, or false if no matches
1969   *         Forum objects have the following attributes:
1970   *         id, type, course, cmid, cmvisible, cmgroupmode, accessallgroups,
1971   *         viewhiddentimedposts
1972   */
1973  function forum_get_readable_forums($userid, $courseid=0) {
1974  
1975      global $CFG, $DB, $USER;
1976      require_once($CFG->dirroot.'/course/lib.php');
1977  
1978      if (!$forummod = $DB->get_record('modules', array('name' => 'forum'))) {
1979          print_error('notinstalled', 'forum');
1980      }
1981  
1982      if ($courseid) {
1983          $courses = $DB->get_records('course', array('id' => $courseid));
1984      } else {
1985          // If no course is specified, then the user can see SITE + his courses.
1986          $courses1 = $DB->get_records('course', array('id' => SITEID));
1987          $courses2 = enrol_get_users_courses($userid, true, array('modinfo'));
1988          $courses = array_merge($courses1, $courses2);
1989      }
1990      if (!$courses) {
1991          return array();
1992      }
1993  
1994      $readableforums = array();
1995  
1996      foreach ($courses as $course) {
1997  
1998          $modinfo = get_fast_modinfo($course);
1999  
2000          if (empty($modinfo->instances['forum'])) {
2001              // hmm, no forums?
2002              continue;
2003          }
2004  
2005          $courseforums = $DB->get_records('forum', array('course' => $course->id));
2006  
2007          foreach ($modinfo->instances['forum'] as $forumid => $cm) {
2008              if (!$cm->uservisible or !isset($courseforums[$forumid])) {
2009                  continue;
2010              }
2011              $context = context_module::instance($cm->id);
2012              $forum = $courseforums[$forumid];
2013              $forum->context = $context;
2014              $forum->cm = $cm;
2015  
2016              if (!has_capability('mod/forum:viewdiscussion', $context)) {
2017                  continue;
2018              }
2019  
2020           /// group access
2021              if (groups_get_activity_groupmode($cm, $course) == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $context)) {
2022  
2023                  $forum->onlygroups = $modinfo->get_groups($cm->groupingid);
2024                  $forum->onlygroups[] = -1;
2025              }
2026  
2027          /// hidden timed discussions
2028              $forum->viewhiddentimedposts = true;
2029              if (!empty($CFG->forum_enabletimedposts)) {
2030                  if (!has_capability('mod/forum:viewhiddentimedposts', $context)) {
2031                      $forum->viewhiddentimedposts = false;
2032                  }
2033              }
2034  
2035          /// qanda access
2036              if ($forum->type == 'qanda'
2037                      && !has_capability('mod/forum:viewqandawithoutposting', $context)) {
2038  
2039                  // We need to check whether the user has posted in the qanda forum.
2040                  $forum->onlydiscussions = array();  // Holds discussion ids for the discussions
2041                                                      // the user is allowed to see in this forum.
2042                  if ($discussionspostedin = forum_discussions_user_has_posted_in($forum->id, $USER->id)) {
2043                      foreach ($discussionspostedin as $d) {
2044                          $forum->onlydiscussions[] = $d->id;
2045                      }
2046                  }
2047              }
2048  
2049              $readableforums[$forum->id] = $forum;
2050          }
2051  
2052          unset($modinfo);
2053  
2054      } // End foreach $courses
2055  
2056      return $readableforums;
2057  }
2058  
2059  /**
2060   * Returns a list of posts found using an array of search terms.
2061   *
2062   * @global object
2063   * @global object
2064   * @global object
2065   * @param array $searchterms array of search terms, e.g. word +word -word
2066   * @param int $courseid if 0, we search through the whole site
2067   * @param int $limitfrom
2068   * @param int $limitnum
2069   * @param int &$totalcount
2070   * @param string $extrasql
2071   * @return array|bool Array of posts found or false
2072   */
2073  function forum_search_posts($searchterms, $courseid=0, $limitfrom=0, $limitnum=50,
2074                              &$totalcount, $extrasql='') {
2075      global $CFG, $DB, $USER;
2076      require_once($CFG->libdir.'/searchlib.php');
2077  
2078      $forums = forum_get_readable_forums($USER->id, $courseid);
2079  
2080      if (count($forums) == 0) {
2081          $totalcount = 0;
2082          return false;
2083      }
2084  
2085      $now = round(time(), -2); // db friendly
2086  
2087      $fullaccess = array();
2088      $where = array();
2089      $params = array();
2090  
2091      foreach ($forums as $forumid => $forum) {
2092          $select = array();
2093  
2094          if (!$forum->viewhiddentimedposts) {
2095              $select[] = "(d.userid = :userid{$forumid} OR (d.timestart < :timestart{$forumid} AND (d.timeend = 0 OR d.timeend > :timeend{$forumid})))";
2096              $params = array_merge($params, array('userid'.$forumid=>$USER->id, 'timestart'.$forumid=>$now, 'timeend'.$forumid=>$now));
2097          }
2098  
2099          $cm = $forum->cm;
2100          $context = $forum->context;
2101  
2102          if ($forum->type == 'qanda'
2103              && !has_capability('mod/forum:viewqandawithoutposting', $context)) {
2104              if (!empty($forum->onlydiscussions)) {
2105                  list($discussionid_sql, $discussionid_params) = $DB->get_in_or_equal($forum->onlydiscussions, SQL_PARAMS_NAMED, 'qanda'.$forumid.'_');
2106                  $params = array_merge($params, $discussionid_params);
2107                  $select[] = "(d.id $discussionid_sql OR p.parent = 0)";
2108              } else {
2109                  $select[] = "p.parent = 0";
2110              }
2111          }
2112  
2113          if (!empty($forum->onlygroups)) {
2114              list($groupid_sql, $groupid_params) = $DB->get_in_or_equal($forum->onlygroups, SQL_PARAMS_NAMED, 'grps'.$forumid.'_');
2115              $params = array_merge($params, $groupid_params);
2116              $select[] = "d.groupid $groupid_sql";
2117          }
2118  
2119          if ($select) {
2120              $selects = implode(" AND ", $select);
2121              $where[] = "(d.forum = :forum{$forumid} AND $selects)";
2122              $params['forum'.$forumid] = $forumid;
2123          } else {
2124              $fullaccess[] = $forumid;
2125          }
2126      }
2127  
2128      if ($fullaccess) {
2129          list($fullid_sql, $fullid_params) = $DB->get_in_or_equal($fullaccess, SQL_PARAMS_NAMED, 'fula');
2130          $params = array_merge($params, $fullid_params);
2131          $where[] = "(d.forum $fullid_sql)";
2132      }
2133  
2134      $selectdiscussion = "(".implode(" OR ", $where).")";
2135  
2136      $messagesearch = '';
2137      $searchstring = '';
2138  
2139      // Need to concat these back together for parser to work.
2140      foreach($searchterms as $searchterm){
2141          if ($searchstring != '') {
2142              $searchstring .= ' ';
2143          }
2144          $searchstring .= $searchterm;
2145      }
2146  
2147      // We need to allow quoted strings for the search. The quotes *should* be stripped
2148      // by the parser, but this should be examined carefully for security implications.
2149      $searchstring = str_replace("\\\"","\"",$searchstring);
2150      $parser = new search_parser();
2151      $lexer = new search_lexer($parser);
2152  
2153      if ($lexer->parse($searchstring)) {
2154          $parsearray = $parser->get_parsed_array();
2155      // Experimental feature under 1.8! MDL-8830
2156      // Use alternative text searches if defined
2157      // This feature only works under mysql until properly implemented for other DBs
2158      // Requires manual creation of text index for forum_posts before enabling it:
2159      // CREATE FULLTEXT INDEX foru_post_tix ON [prefix]forum_posts (subject, message)
2160      // Experimental feature under 1.8! MDL-8830
2161          if (!empty($CFG->forum_usetextsearches)) {
2162              list($messagesearch, $msparams) = search_generate_text_SQL($parsearray, 'p.message', 'p.subject',
2163                                                   'p.userid', 'u.id', 'u.firstname',
2164                                                   'u.lastname', 'p.modified', 'd.forum');
2165          } else {
2166              list($messagesearch, $msparams) = search_generate_SQL($parsearray, 'p.message', 'p.subject',
2167                                                   'p.userid', 'u.id', 'u.firstname',
2168                                                   'u.lastname', 'p.modified', 'd.forum');
2169          }
2170          $params = array_merge($params, $msparams);
2171      }
2172  
2173      $fromsql = "{forum_posts} p,
2174                    {forum_discussions} d,
2175                    {user} u";
2176  
2177      $selectsql = " $messagesearch
2178                 AND p.discussion = d.id
2179                 AND p.userid = u.id
2180                 AND $selectdiscussion
2181                     $extrasql";
2182  
2183      $countsql = "SELECT COUNT(*)
2184                     FROM $fromsql
2185                    WHERE $selectsql";
2186  
2187      $allnames = get_all_user_name_fields(true, 'u');
2188      $searchsql = "SELECT p.*,
2189                           d.forum,
2190                           $allnames,
2191                           u.email,
2192                           u.picture,
2193                           u.imagealt
2194                      FROM $fromsql
2195                     WHERE $selectsql
2196                  ORDER BY p.modified DESC";
2197  
2198      $totalcount = $DB->count_records_sql($countsql, $params);
2199  
2200      return $DB->get_records_sql($searchsql, $params, $limitfrom, $limitnum);
2201  }
2202  
2203  /**
2204   * Returns a list of all new posts that have not been mailed yet
2205   *
2206   * @param int $starttime posts created after this time
2207   * @param int $endtime posts created before this
2208   * @param int $now used for timed discussions only
2209   * @return array
2210   */
2211  function forum_get_unmailed_posts($starttime, $endtime, $now=null) {
2212      global $CFG, $DB;
2213  
2214      $params = array();
2215      $params['mailed'] = FORUM_MAILED_PENDING;
2216      $params['ptimestart'] = $starttime;
2217      $params['ptimeend'] = $endtime;
2218      $params['mailnow'] = 1;
2219  
2220      if (!empty($CFG->forum_enabletimedposts)) {
2221          if (empty($now)) {
2222              $now = time();
2223          }
2224          $timedsql = "AND (d.timestart < :dtimestart AND (d.timeend = 0 OR d.timeend > :dtimeend))";
2225          $params['dtimestart'] = $now;
2226          $params['dtimeend'] = $now;
2227      } else {
2228          $timedsql = "";
2229      }
2230  
2231      return $DB->get_records_sql("SELECT p.*, d.course, d.forum
2232                                   FROM {forum_posts} p
2233                                   JOIN {forum_discussions} d ON d.id = p.discussion
2234                                   WHERE p.mailed = :mailed
2235                                   AND p.created >= :ptimestart
2236                                   AND (p.created < :ptimeend OR p.mailnow = :mailnow)
2237                                   $timedsql
2238                                   ORDER BY p.modified ASC", $params);
2239  }
2240  
2241  /**
2242   * Marks posts before a certain time as being mailed already
2243   *
2244   * @global object
2245   * @global object
2246   * @param int $endtime
2247   * @param int $now Defaults to time()
2248   * @return bool
2249   */
2250  function forum_mark_old_posts_as_mailed($endtime, $now=null) {
2251      global $CFG, $DB;
2252  
2253      if (empty($now)) {
2254          $now = time();
2255      }
2256  
2257      $params = array();
2258      $params['mailedsuccess'] = FORUM_MAILED_SUCCESS;
2259      $params['now'] = $now;
2260      $params['endtime'] = $endtime;
2261      $params['mailnow'] = 1;
2262      $params['mailedpending'] = FORUM_MAILED_PENDING;
2263  
2264      if (empty($CFG->forum_enabletimedposts)) {
2265          return $DB->execute("UPDATE {forum_posts}
2266                               SET mailed = :mailedsuccess
2267                               WHERE (created < :endtime OR mailnow = :mailnow)
2268                               AND mailed = :mailedpending", $params);
2269      } else {
2270          return $DB->execute("UPDATE {forum_posts}
2271                               SET mailed = :mailedsuccess
2272                               WHERE discussion NOT IN (SELECT d.id
2273                                                        FROM {forum_discussions} d
2274                                                        WHERE d.timestart > :now)
2275                               AND (created < :endtime OR mailnow = :mailnow)
2276                               AND mailed = :mailedpending", $params);
2277      }
2278  }
2279  
2280  /**
2281   * Get all the posts for a user in a forum suitable for forum_print_post
2282   *
2283   * @global object
2284   * @global object
2285   * @uses CONTEXT_MODULE
2286   * @return array
2287   */
2288  function forum_get_user_posts($forumid, $userid) {
2289      global $CFG, $DB;
2290  
2291      $timedsql = "";
2292      $params = array($forumid, $userid);
2293  
2294      if (!empty($CFG->forum_enabletimedposts)) {
2295          $cm = get_coursemodule_from_instance('forum', $forumid);
2296          if (!has_capability('mod/forum:viewhiddentimedposts' , context_module::instance($cm->id))) {
2297              $now = time();
2298              $timedsql = "AND (d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?))";
2299              $params[] = $now;
2300              $params[] = $now;
2301          }
2302      }
2303  
2304      $allnames = get_all_user_name_fields(true, 'u');
2305      return $DB->get_records_sql("SELECT p.*, d.forum, $allnames, u.email, u.picture, u.imagealt
2306                                FROM {forum} f
2307                                     JOIN {forum_discussions} d ON d.forum = f.id
2308                                     JOIN {forum_posts} p       ON p.discussion = d.id
2309                                     JOIN {user} u              ON u.id = p.userid
2310                               WHERE f.id = ?
2311                                     AND p.userid = ?
2312                                     $timedsql
2313                            ORDER BY p.modified ASC", $params);
2314  }
2315  
2316  /**
2317   * Get all the discussions user participated in
2318   *
2319   * @global object
2320   * @global object
2321   * @uses CONTEXT_MODULE
2322   * @param int $forumid
2323   * @param int $userid
2324   * @return array Array or false
2325   */
2326  function forum_get_user_involved_discussions($forumid, $userid) {
2327      global $CFG, $DB;
2328  
2329      $timedsql = "";
2330      $params = array($forumid, $userid);
2331      if (!empty($CFG->forum_enabletimedposts)) {
2332          $cm = get_coursemodule_from_instance('forum', $forumid);
2333          if (!has_capability('mod/forum:viewhiddentimedposts' , context_module::instance($cm->id))) {
2334              $now = time();
2335              $timedsql = "AND (d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?))";
2336              $params[] = $now;
2337              $params[] = $now;
2338          }
2339      }
2340  
2341      return $DB->get_records_sql("SELECT DISTINCT d.*
2342                                FROM {forum} f
2343                                     JOIN {forum_discussions} d ON d.forum = f.id
2344                                     JOIN {forum_posts} p       ON p.discussion = d.id
2345                               WHERE f.id = ?
2346                                     AND p.userid = ?
2347                                     $timedsql", $params);
2348  }
2349  
2350  /**
2351   * Get all the posts for a user in a forum suitable for forum_print_post
2352   *
2353   * @global object
2354   * @global object
2355   * @param int $forumid
2356   * @param int $userid
2357   * @return array of counts or false
2358   */
2359  function forum_count_user_posts($forumid, $userid) {
2360      global $CFG, $DB;
2361  
2362      $timedsql = "";
2363      $params = array($forumid, $userid);
2364      if (!empty($CFG->forum_enabletimedposts)) {
2365          $cm = get_coursemodule_from_instance('forum', $forumid);
2366          if (!has_capability('mod/forum:viewhiddentimedposts' , context_module::instance($cm->id))) {
2367              $now = time();
2368              $timedsql = "AND (d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?))";
2369              $params[] = $now;
2370              $params[] = $now;
2371          }
2372      }
2373  
2374      return $DB->get_record_sql("SELECT COUNT(p.id) AS postcount, MAX(p.modified) AS lastpost
2375                               FROM {forum} f
2376                                    JOIN {forum_discussions} d ON d.forum = f.id
2377                                    JOIN {forum_posts} p       ON p.discussion = d.id
2378                                    JOIN {user} u              ON u.id = p.userid
2379                              WHERE f.id = ?
2380                                    AND p.userid = ?
2381                                    $timedsql", $params);
2382  }
2383  
2384  /**
2385   * Given a log entry, return the forum post details for it.
2386   *
2387   * @global object
2388   * @global object
2389   * @param object $log
2390   * @return array|null
2391   */
2392  function forum_get_post_from_log($log) {
2393      global $CFG, $DB;
2394  
2395      $allnames = get_all_user_name_fields(true, 'u');
2396      if ($log->action == "add post") {
2397  
2398          return $DB->get_record_sql("SELECT p.*, f.type AS forumtype, d.forum, d.groupid, $allnames, u.email, u.picture
2399                                   FROM {forum_discussions} d,
2400                                        {forum_posts} p,
2401                                        {forum} f,
2402                                        {user} u
2403                                  WHERE p.id = ?
2404                                    AND d.id = p.discussion
2405                                    AND p.userid = u.id
2406                                    AND u.deleted <> '1'
2407                                    AND f.id = d.forum", array($log->info));
2408  
2409  
2410      } else if ($log->action == "add discussion") {
2411  
2412          return $DB->get_record_sql("SELECT p.*, f.type AS forumtype, d.forum, d.groupid, $allnames, u.email, u.picture
2413                                   FROM {forum_discussions} d,
2414                                        {forum_posts} p,
2415                                        {forum} f,
2416                                        {user} u
2417                                  WHERE d.id = ?
2418                                    AND d.firstpost = p.id
2419                                    AND p.userid = u.id
2420                                    AND u.deleted <> '1'
2421                                    AND f.id = d.forum", array($log->info));
2422      }
2423      return NULL;
2424  }
2425  
2426  /**
2427   * Given a discussion id, return the first post from the discussion
2428   *
2429   * @global object
2430   * @global object
2431   * @param int $dicsussionid
2432   * @return array
2433   */
2434  function forum_get_firstpost_from_discussion($discussionid) {
2435      global $CFG, $DB;
2436  
2437      return $DB->get_record_sql("SELECT p.*
2438                               FROM {forum_discussions} d,
2439                                    {forum_posts} p
2440                              WHERE d.id = ?
2441                                AND d.firstpost = p.id ", array($discussionid));
2442  }
2443  
2444  /**
2445   * Returns an array of counts of replies to each discussion
2446   *
2447   * @global object
2448   * @global object
2449   * @param int $forumid
2450   * @param string $forumsort
2451   * @param int $limit
2452   * @param int $page
2453   * @param int $perpage
2454   * @return array
2455   */
2456  function forum_count_discussion_replies($forumid, $forumsort="", $limit=-1, $page=-1, $perpage=0) {
2457      global $CFG, $DB;
2458  
2459      if ($limit > 0) {
2460          $limitfrom = 0;
2461          $limitnum  = $limit;
2462      } else if ($page != -1) {
2463          $limitfrom = $page*$perpage;
2464          $limitnum  = $perpage;
2465      } else {
2466          $limitfrom = 0;
2467          $limitnum  = 0;
2468      }
2469  
2470      if ($forumsort == "") {
2471          $orderby = "";
2472          $groupby = "";
2473  
2474      } else {
2475          $orderby = "ORDER BY $forumsort";
2476          $groupby = ", ".strtolower($forumsort);
2477          $groupby = str_replace('desc', '', $groupby);
2478          $groupby = str_replace('asc', '', $groupby);
2479      }
2480  
2481      if (($limitfrom == 0 and $limitnum == 0) or $forumsort == "") {
2482          $sql = "SELECT p.discussion, COUNT(p.id) AS replies, MAX(p.id) AS lastpostid
2483                    FROM {forum_posts} p
2484                         JOIN {forum_discussions} d ON p.discussion = d.id
2485                   WHERE p.parent > 0 AND d.forum = ?
2486                GROUP BY p.discussion";
2487          return $DB->get_records_sql($sql, array($forumid));
2488  
2489      } else {
2490          $sql = "SELECT p.discussion, (COUNT(p.id) - 1) AS replies, MAX(p.id) AS lastpostid
2491                    FROM {forum_posts} p
2492                         JOIN {forum_discussions} d ON p.discussion = d.id
2493                   WHERE d.forum = ?
2494                GROUP BY p.discussion $groupby $orderby";
2495          return $DB->get_records_sql($sql, array($forumid), $limitfrom, $limitnum);
2496      }
2497  }
2498  
2499  /**
2500   * @global object
2501   * @global object
2502   * @global object
2503   * @staticvar array $cache
2504   * @param object $forum
2505   * @param object $cm
2506   * @param object $course
2507   * @return mixed
2508   */
2509  function forum_count_discussions($forum, $cm, $course) {
2510      global $CFG, $DB, $USER;
2511  
2512      static $cache = array();
2513  
2514      $now = round(time(), -2); // db cache friendliness
2515  
2516      $params = array($course->id);
2517  
2518      if (!isset($cache[$course->id])) {
2519          if (!empty($CFG->forum_enabletimedposts)) {
2520              $timedsql = "AND d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?)";
2521              $params[] = $now;
2522              $params[] = $now;
2523          } else {
2524              $timedsql = "";
2525          }
2526  
2527          $sql = "SELECT f.id, COUNT(d.id) as dcount
2528                    FROM {forum} f
2529                         JOIN {forum_discussions} d ON d.forum = f.id
2530                   WHERE f.course = ?
2531                         $timedsql
2532                GROUP BY f.id";
2533  
2534          if ($counts = $DB->get_records_sql($sql, $params)) {
2535              foreach ($counts as $count) {
2536                  $counts[$count->id] = $count->dcount;
2537              }
2538              $cache[$course->id] = $counts;
2539          } else {
2540              $cache[$course->id] = array();
2541          }
2542      }
2543  
2544      if (empty($cache[$course->id][$forum->id])) {
2545          return 0;
2546      }
2547  
2548      $groupmode = groups_get_activity_groupmode($cm, $course);
2549  
2550      if ($groupmode != SEPARATEGROUPS) {
2551          return $cache[$course->id][$forum->id];
2552      }
2553  
2554      if (has_capability('moodle/site:accessallgroups', context_module::instance($cm->id))) {
2555          return $cache[$course->id][$forum->id];
2556      }
2557  
2558      require_once($CFG->dirroot.'/course/lib.php');
2559  
2560      $modinfo = get_fast_modinfo($course);
2561  
2562      $mygroups = $modinfo->get_groups($cm->groupingid);
2563  
2564      // add all groups posts
2565      $mygroups[-1] = -1;
2566  
2567      list($mygroups_sql, $params) = $DB->get_in_or_equal($mygroups);
2568      $params[] = $forum->id;
2569  
2570      if (!empty($CFG->forum_enabletimedposts)) {
2571          $timedsql = "AND d.timestart < $now AND (d.timeend = 0 OR d.timeend > $now)";
2572          $params[] = $now;
2573          $params[] = $now;
2574      } else {
2575          $timedsql = "";
2576      }
2577  
2578      $sql = "SELECT COUNT(d.id)
2579                FROM {forum_discussions} d
2580               WHERE d.groupid $mygroups_sql AND d.forum = ?
2581                     $timedsql";
2582  
2583      return $DB->get_field_sql($sql, $params);
2584  }
2585  
2586  /**
2587   * Get all discussions in a forum
2588   *
2589   * @global object
2590   * @global object
2591   * @global object
2592   * @uses CONTEXT_MODULE
2593   * @uses VISIBLEGROUPS
2594   * @param object $cm
2595   * @param string $forumsort
2596   * @param bool $fullpost
2597   * @param int $unused
2598   * @param int $limit
2599   * @param bool $userlastmodified
2600   * @param int $page
2601   * @param int $perpage
2602   * @return array
2603   */
2604  function forum_get_discussions($cm, $forumsort="d.timemodified DESC", $fullpost=true, $unused=-1, $limit=-1, $userlastmodified=false, $page=-1, $perpage=0) {
2605      global $CFG, $DB, $USER;
2606  
2607      $timelimit = '';
2608  
2609      $now = round(time(), -2);
2610      $params = array($cm->instance);
2611  
2612      $modcontext = context_module::instance($cm->id);
2613  
2614      if (!has_capability('mod/forum:viewdiscussion', $modcontext)) { /// User must have perms to view discussions
2615          return array();
2616      }
2617  
2618      if (!empty($CFG->forum_enabletimedposts)) { /// Users must fulfill timed posts
2619  
2620          if (!has_capability('mod/forum:viewhiddentimedposts', $modcontext)) {
2621              $timelimit = " AND ((d.timestart <= ? AND (d.timeend = 0 OR d.timeend > ?))";
2622              $params[] = $now;
2623              $params[] = $now;
2624              if (isloggedin()) {
2625                  $timelimit .= " OR d.userid = ?";
2626                  $params[] = $USER->id;
2627              }
2628              $timelimit .= ")";
2629          }
2630      }
2631  
2632      if ($limit > 0) {
2633          $limitfrom = 0;
2634          $limitnum  = $limit;
2635      } else if ($page != -1) {
2636          $limitfrom = $page*$perpage;
2637          $limitnum  = $perpage;
2638      } else {
2639          $limitfrom = 0;
2640          $limitnum  = 0;
2641      }
2642  
2643      $groupmode    = groups_get_activity_groupmode($cm);
2644      $currentgroup = groups_get_activity_group($cm);
2645  
2646      if ($groupmode) {
2647          if (empty($modcontext)) {
2648              $modcontext = context_module::instance($cm->id);
2649          }
2650  
2651          if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $modcontext)) {
2652              if ($currentgroup) {
2653                  $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
2654                  $params[] = $currentgroup;
2655              } else {
2656                  $groupselect = "";
2657              }
2658  
2659          } else {
2660              //seprate groups without access all
2661              if ($currentgroup) {
2662                  $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
2663                  $params[] = $currentgroup;
2664              } else {
2665                  $groupselect = "AND d.groupid = -1";
2666              }
2667          }
2668      } else {
2669          $groupselect = "";
2670      }
2671  
2672  
2673      if (empty($forumsort)) {
2674          $forumsort = "d.timemodified DESC";
2675      }
2676      if (empty($fullpost)) {
2677          $postdata = "p.id,p.subject,p.modified,p.discussion,p.userid";
2678      } else {
2679          $postdata = "p.*";
2680      }
2681  
2682      if (empty($userlastmodified)) {  // We don't need to know this
2683          $umfields = "";
2684          $umtable  = "";
2685      } else {
2686          $umfields = ', ' . get_all_user_name_fields(true, 'um', null, 'um');
2687          $umtable  = " LEFT JOIN {user} um ON (d.usermodified = um.id)";
2688      }
2689  
2690      $allnames = get_all_user_name_fields(true, 'u');
2691      $sql = "SELECT $postdata, d.name, d.timemodified, d.usermodified, d.groupid, d.timestart, d.timeend, $allnames,
2692                     u.email, u.picture, u.imagealt $umfields
2693                FROM {forum_discussions} d
2694                     JOIN {forum_posts} p ON p.discussion = d.id
2695                     JOIN {user} u ON p.userid = u.id
2696                     $umtable
2697               WHERE d.forum = ? AND p.parent = 0
2698                     $timelimit $groupselect
2699            ORDER BY $forumsort";
2700      return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
2701  }
2702  
2703  /**
2704   * Gets the neighbours (previous and next) of a discussion.
2705   *
2706   * The calculation is based on the timemodified of the discussion and does not handle
2707   * the neighbours having an identical timemodified. The reason is that we do not have any
2708   * other mean to sort the records, e.g. we cannot use IDs as a greater ID can have a lower
2709   * timemodified.
2710   *
2711   * Please note that this does not check whether or not the discussion passed is accessible
2712   * by the user, it simply uses it as a reference to find the neighbours. On the other hand,
2713   * the returned neighbours are checked and are accessible to the current user.
2714   *
2715   * @param object $cm The CM record.
2716   * @param object $discussion The discussion record.
2717   * @return array That always contains the keys 'prev' and 'next'. When there is a result
2718   *               they contain the record with minimal information such as 'id' and 'name'.
2719   *               When the neighbour is not found the value is false.
2720   */
2721  function forum_get_discussion_neighbours($cm, $discussion) {
2722      global $CFG, $DB, $USER;
2723  
2724      if ($cm->instance != $discussion->forum) {
2725          throw new coding_exception('Discussion is not part of the same forum.');
2726      }
2727  
2728      $neighbours = array('prev' => false, 'next' => false);
2729      $now = round(time(), -2);
2730      $params = array();
2731  
2732      $modcontext = context_module::instance($cm->id);
2733      $groupmode    = groups_get_activity_groupmode($cm);
2734      $currentgroup = groups_get_activity_group($cm);
2735  
2736      // Users must fulfill timed posts.
2737      $timelimit = '';
2738      if (!empty($CFG->forum_enabletimedposts)) {
2739          if (!has_capability('mod/forum:viewhiddentimedposts', $modcontext)) {
2740              $timelimit = ' AND ((d.timestart <= :tltimestart AND (d.timeend = 0 OR d.timeend > :tltimeend))';
2741              $params['tltimestart'] = $now;
2742              $params['tltimeend'] = $now;
2743              if (isloggedin()) {
2744                  $timelimit .= ' OR d.userid = :tluserid';
2745                  $params['tluserid'] = $USER->id;
2746              }
2747              $timelimit .= ')';
2748          }
2749      }
2750  
2751      // Limiting to posts accessible according to groups.
2752      $groupselect = '';
2753      if ($groupmode) {
2754          if ($groupmode == VISIBLEGROUPS || has_capability('moodle/site:accessallgroups', $modcontext)) {
2755              if ($currentgroup) {
2756                  $groupselect = 'AND (d.groupid = :groupid OR d.groupid = -1)';
2757                  $params['groupid'] = $currentgroup;
2758              }
2759          } else {
2760              if ($currentgroup) {
2761                  $groupselect = 'AND (d.groupid = :groupid OR d.groupid = -1)';
2762                  $params['groupid'] = $currentgroup;
2763              } else {
2764                  $groupselect = 'AND d.groupid = -1';
2765              }
2766          }
2767      }
2768  
2769      $params['forumid'] = $cm->instance;
2770      $params['discid'] = $discussion->id;
2771      $params['disctimemodified'] = $discussion->timemodified;
2772  
2773      $sql = "SELECT d.id, d.name, d.timemodified, d.groupid, d.timestart, d.timeend
2774                FROM {forum_discussions} d
2775               WHERE d.forum = :forumid
2776                 AND d.id <> :discid
2777                     $timelimit
2778                     $groupselect";
2779  
2780      $prevsql = $sql . " AND d.timemodified < :disctimemodified
2781                     ORDER BY d.timemodified DESC";
2782  
2783      $nextsql = $sql . " AND d.timemodified > :disctimemodified
2784                     ORDER BY d.timemodified ASC";
2785  
2786      $neighbours['prev'] = $DB->get_record_sql($prevsql, $params, IGNORE_MULTIPLE);
2787      $neighbours['next'] = $DB->get_record_sql($nextsql, $params, IGNORE_MULTIPLE);
2788  
2789      return $neighbours;
2790  }
2791  
2792  /**
2793   *
2794   * @global object
2795   * @global object
2796   * @global object
2797   * @uses CONTEXT_MODULE
2798   * @uses VISIBLEGROUPS
2799   * @param object $cm
2800   * @return array
2801   */
2802  function forum_get_discussions_unread($cm) {
2803      global $CFG, $DB, $USER;
2804  
2805      $now = round(time(), -2);
2806      $cutoffdate = $now - ($CFG->forum_oldpostdays*24*60*60);
2807  
2808      $params = array();
2809      $groupmode    = groups_get_activity_groupmode($cm);
2810      $currentgroup = groups_get_activity_group($cm);
2811  
2812      if ($groupmode) {
2813          $modcontext = context_module::instance($cm->id);
2814  
2815          if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $modcontext)) {
2816              if ($currentgroup) {
2817                  $groupselect = "AND (d.groupid = :currentgroup OR d.groupid = -1)";
2818                  $params['currentgroup'] = $currentgroup;
2819              } else {
2820                  $groupselect = "";
2821              }
2822  
2823          } else {
2824              //separate groups without access all
2825              if ($currentgroup) {
2826                  $groupselect = "AND (d.groupid = :currentgroup OR d.groupid = -1)";
2827                  $params['currentgroup'] = $currentgroup;
2828              } else {
2829                  $groupselect = "AND d.groupid = -1";
2830              }
2831          }
2832      } else {
2833          $groupselect = "";
2834      }
2835  
2836      if (!empty($CFG->forum_enabletimedposts)) {
2837          $timedsql = "AND d.timestart < :now1 AND (d.timeend = 0 OR d.timeend > :now2)";
2838          $params['now1'] = $now;
2839          $params['now2'] = $now;
2840      } else {
2841          $timedsql = "";
2842      }
2843  
2844      $sql = "SELECT d.id, COUNT(p.id) AS unread
2845                FROM {forum_discussions} d
2846                     JOIN {forum_posts} p     ON p.discussion = d.id
2847                     LEFT JOIN {forum_read} r ON (r.postid = p.id AND r.userid = $USER->id)
2848               WHERE d.forum = {$cm->instance}
2849                     AND p.modified >= :cutoffdate AND r.id is NULL
2850                     $groupselect
2851                     $timedsql
2852            GROUP BY d.id";
2853      $params['cutoffdate'] = $cutoffdate;
2854  
2855      if ($unreads = $DB->get_records_sql($sql, $params)) {
2856          foreach ($unreads as $unread) {
2857              $unreads[$unread->id] = $unread->unread;
2858          }
2859          return $unreads;
2860      } else {
2861          return array();
2862      }
2863  }
2864  
2865  /**
2866   * @global object
2867   * @global object
2868   * @global object
2869   * @uses CONEXT_MODULE
2870   * @uses VISIBLEGROUPS
2871   * @param object $cm
2872   * @return array
2873   */
2874  function forum_get_discussions_count($cm) {
2875      global $CFG, $DB, $USER;
2876  
2877      $now = round(time(), -2);
2878      $params = array($cm->instance);
2879      $groupmode    = groups_get_activity_groupmode($cm);
2880      $currentgroup = groups_get_activity_group($cm);
2881  
2882      if ($groupmode) {
2883          $modcontext = context_module::instance($cm->id);
2884  
2885          if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $modcontext)) {
2886              if ($currentgroup) {
2887                  $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
2888                  $params[] = $currentgroup;
2889              } else {
2890                  $groupselect = "";
2891              }
2892  
2893          } else {
2894              //seprate groups without access all
2895              if ($currentgroup) {
2896                  $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
2897                  $params[] = $currentgroup;
2898              } else {
2899                  $groupselect = "AND d.groupid = -1";
2900              }
2901          }
2902      } else {
2903          $groupselect = "";
2904      }
2905  
2906      $cutoffdate = $now - ($CFG->forum_oldpostdays*24*60*60);
2907  
2908      $timelimit = "";
2909  
2910      if (!empty($CFG->forum_enabletimedposts)) {
2911  
2912          $modcontext = context_module::instance($cm->id);
2913  
2914          if (!has_capability('mod/forum:viewhiddentimedposts', $modcontext)) {
2915              $timelimit = " AND ((d.timestart <= ? AND (d.timeend = 0 OR d.timeend > ?))";
2916              $params[] = $now;
2917              $params[] = $now;
2918              if (isloggedin()) {
2919                  $timelimit .= " OR d.userid = ?";
2920                  $params[] = $USER->id;
2921              }
2922              $timelimit .= ")";
2923          }
2924      }
2925  
2926      $sql = "SELECT COUNT(d.id)
2927                FROM {forum_discussions} d
2928                     JOIN {forum_posts} p ON p.discussion = d.id
2929               WHERE d.forum = ? AND p.parent = 0
2930                     $groupselect $timelimit";
2931  
2932      return $DB->get_field_sql($sql, $params);
2933  }
2934  
2935  
2936  // OTHER FUNCTIONS ///////////////////////////////////////////////////////////
2937  
2938  
2939  /**
2940   * @global object
2941   * @global object
2942   * @param int $courseid
2943   * @param string $type
2944   */
2945  function forum_get_course_forum($courseid, $type) {
2946  // How to set up special 1-per-course forums
2947      global $CFG, $DB, $OUTPUT, $USER;
2948  
2949      if ($forums = $DB->get_records_select("forum", "course = ? AND type = ?", array($courseid, $type), "id ASC")) {
2950          // There should always only be ONE, but with the right combination of
2951          // errors there might be more.  In this case, just return the oldest one (lowest ID).
2952          foreach ($forums as $forum) {
2953              return $forum;   // ie the first one
2954          }
2955      }
2956  
2957      // Doesn't exist, so create one now.
2958      $forum = new stdClass();
2959      $forum->course = $courseid;
2960      $forum->type = "$type";
2961      if (!empty($USER->htmleditor)) {
2962          $forum->introformat = $USER->htmleditor;
2963      }
2964      switch ($forum->type) {
2965          case "news":
2966              $forum->name  = get_string("namenews", "forum");
2967              $forum->intro = get_string("intronews", "forum");
2968              $forum->forcesubscribe = FORUM_FORCESUBSCRIBE;
2969              $forum->assessed = 0;
2970              if ($courseid == SITEID) {
2971                  $forum->name  = get_string("sitenews");
2972                  $forum->forcesubscribe = 0;
2973              }
2974              break;
2975          case "social":
2976              $forum->name  = get_string("namesocial", "forum");
2977              $forum->intro = get_string("introsocial", "forum");
2978              $forum->assessed = 0;
2979              $forum->forcesubscribe = 0;
2980              break;
2981          case "blog":
2982              $forum->name = get_string('blogforum', 'forum');
2983              $forum->intro = get_string('introblog', 'forum');
2984              $forum->assessed = 0;
2985              $forum->forcesubscribe = 0;
2986              break;
2987          default:
2988              echo $OUTPUT->notification("That forum type doesn't exist!");
2989              return false;
2990              break;
2991      }
2992  
2993      $forum->timemodified = time();
2994      $forum->id = $DB->insert_record("forum", $forum);
2995  
2996      if (! $module = $DB->get_record("modules", array("name" => "forum"))) {
2997          echo $OUTPUT->notification("Could not find forum module!!");
2998          return false;
2999      }
3000      $mod = new stdClass();
3001      $mod->course = $courseid;
3002      $mod->module = $module->id;
3003      $mod->instance = $forum->id;
3004      $mod->section = 0;
3005      include_once("$CFG->dirroot/course/lib.php");
3006      if (! $mod->coursemodule = add_course_module($mod) ) {
3007          echo $OUTPUT->notification("Could not add a new course module to the course '" . $courseid . "'");
3008          return false;
3009      }
3010      $sectionid = course_add_cm_to_section($courseid, $mod->coursemodule, 0);
3011      return $DB->get_record("forum", array("id" => "$forum->id"));
3012  }
3013  
3014  
3015  /**
3016   * Given the data about a posting, builds up the HTML to display it and
3017   * returns the HTML in a string.  This is designed for sending via HTML email.
3018   *
3019   * @global object
3020   * @param object $course
3021   * @param object $cm
3022   * @param object $forum
3023   * @param object $discussion
3024   * @param object $post
3025   * @param object $userform
3026   * @param object $userto
3027   * @param bool $ownpost
3028   * @param bool $reply
3029   * @param bool $link
3030   * @param bool $rate
3031   * @param string $footer
3032   * @return string
3033   */
3034  function forum_make_mail_post($course, $cm, $forum, $discussion, $post, $userfrom, $userto,
3035                                $ownpost=false, $reply=false, $link=false, $rate=false, $footer="") {
3036  
3037      global $CFG, $OUTPUT;
3038  
3039      $modcontext = context_module::instance($cm->id);
3040  
3041      if (!isset($userto->viewfullnames[$forum->id])) {
3042          $viewfullnames = has_capability('moodle/site:viewfullnames', $modcontext, $userto->id);
3043      } else {
3044          $viewfullnames = $userto->viewfullnames[$forum->id];
3045      }
3046  
3047      // add absolute file links
3048      $post->message = file_rewrite_pluginfile_urls($post->message, 'pluginfile.php', $modcontext->id, 'mod_forum', 'post', $post->id);
3049  
3050      // format the post body
3051      $options = new stdClass();
3052      $options->para = true;
3053      $formattedtext = format_text($post->message, $post->messageformat, $options, $course->id);
3054  
3055      $output = '<table border="0" cellpadding="3" cellspacing="0" class="forumpost">';
3056  
3057      $output .= '<tr class="header"><td width="35" valign="top" class="picture left">';
3058      $output .= $OUTPUT->user_picture($userfrom, array('courseid'=>$course->id));
3059      $output .= '</td>';
3060  
3061      if ($post->parent) {
3062          $output .= '<td class="topic">';
3063      } else {
3064          $output .= '<td class="topic starter">';
3065      }
3066      $output .= '<div class="subject">'.format_string($post->subject).'</div>';
3067  
3068      $fullname = fullname($userfrom, $viewfullnames);
3069      $by = new stdClass();
3070      $by->name = '<a href="'.$CFG->wwwroot.'/user/view.php?id='.$userfrom->id.'&amp;course='.$course->id.'">'.$fullname.'</a>';
3071      $by->date = userdate($post->modified, '', $userto->timezone);
3072      $output .= '<div class="author">'.get_string('bynameondate', 'forum', $by).'</div>';
3073  
3074      $output .= '</td></tr>';
3075  
3076      $output .= '<tr><td class="left side" valign="top">';
3077  
3078      if (isset($userfrom->groups)) {
3079          $groups = $userfrom->groups[$forum->id];
3080      } else {
3081          $groups = groups_get_all_groups($course->id, $userfrom->id, $cm->groupingid);
3082      }
3083  
3084      if ($groups) {
3085          $output .= print_group_picture($groups, $course->id, false, true, true);
3086      } else {
3087          $output .= '&nbsp;';
3088      }
3089  
3090      $output .= '</td><td class="content">';
3091  
3092      $attachments = forum_print_attachments($post, $cm, 'html');
3093      if ($attachments !== '') {
3094          $output .= '<div class="attachments">';
3095          $output .= $attachments;
3096          $output .= '</div>';
3097      }
3098  
3099      $output .= $formattedtext;
3100  
3101  // Commands
3102      $commands = array();
3103  
3104      if ($post->parent) {
3105          $commands[] = '<a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.
3106                        $post->discussion.'&amp;parent='.$post->parent.'">'.get_string('parent', 'forum').'</a>';
3107      }
3108  
3109      if ($reply) {
3110          $commands[] = '<a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/post.php?reply='.$post->id.'">'.
3111                        get_string('reply', 'forum').'</a>';
3112      }
3113  
3114      $output .= '<div class="commands">';
3115      $output .= implode(' | ', $commands);
3116      $output .= '</div>';
3117  
3118  // Context link to post if required
3119      if ($link) {
3120          $output .= '<div class="link">';
3121          $output .= '<a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$post->discussion.'#p'.$post->id.'">'.
3122                       get_string('postincontext', 'forum').'</a>';
3123          $output .= '</div>';
3124      }
3125  
3126      if ($footer) {
3127          $output .= '<div class="footer">'.$footer.'</div>';
3128      }
3129      $output .= '</td></tr></table>'."\n\n";
3130  
3131      return $output;
3132  }
3133  
3134  /**
3135   * Print a forum post
3136   *
3137   * @global object
3138   * @global object
3139   * @uses FORUM_MODE_THREADED
3140   * @uses PORTFOLIO_FORMAT_PLAINHTML
3141   * @uses PORTFOLIO_FORMAT_FILE
3142   * @uses PORTFOLIO_FORMAT_RICHHTML
3143   * @uses PORTFOLIO_ADD_TEXT_LINK
3144   * @uses CONTEXT_MODULE
3145   * @param object $post The post to print.
3146   * @param object $discussion
3147   * @param object $forum
3148   * @param object $cm
3149   * @param object $course
3150   * @param boolean $ownpost Whether this post belongs to the current user.
3151   * @param boolean $reply Whether to print a 'reply' link at the bottom of the message.
3152   * @param boolean $link Just print a shortened version of the post as a link to the full post.
3153   * @param string $footer Extra stuff to print after the message.
3154   * @param string $highlight Space-separated list of terms to highlight.
3155   * @param int $post_read true, false or -99. If we already know whether this user
3156   *          has read this post, pass that in, otherwise, pass in -99, and this
3157   *          function will work it out.
3158   * @param boolean $dummyifcantsee When forum_user_can_see_post says that
3159   *          the current user can't see this post, if this argument is true
3160   *          (the default) then print a dummy 'you can't see this post' post.
3161   *          If false, don't output anything at all.
3162   * @param bool|null $istracked
3163   * @return void
3164   */
3165  function forum_print_post($post, $discussion, $forum, &$cm, $course, $ownpost=false, $reply=false, $link=false,
3166                            $footer="", $highlight="", $postisread=null, $dummyifcantsee=true, $istracked=null, $return=false) {
3167      global $USER, $CFG, $OUTPUT;
3168  
3169      require_once($CFG->libdir . '/filelib.php');
3170  
3171      // String cache
3172      static $str;
3173  
3174      $modcontext = context_module::instance($cm->id);
3175  
3176      $post->course = $course->id;
3177      $post->forum  = $forum->id;
3178      $post->message = file_rewrite_pluginfile_urls($post->message, 'pluginfile.php', $modcontext->id, 'mod_forum', 'post', $post->id);
3179      if (!empty($CFG->enableplagiarism)) {
3180          require_once($CFG->libdir.'/plagiarismlib.php');
3181          $post->message .= plagiarism_get_links(array('userid' => $post->userid,
3182              'content' => $post->message,
3183              'cmid' => $cm->id,
3184              'course' => $post->course,
3185              'forum' => $post->forum));
3186      }
3187  
3188      // caching
3189      if (!isset($cm->cache)) {
3190          $cm->cache = new stdClass;
3191      }
3192  
3193      if (!isset($cm->cache->caps)) {
3194          $cm->cache->caps = array();
3195          $cm->cache->caps['mod/forum:viewdiscussion']   = has_capability('mod/forum:viewdiscussion', $modcontext);
3196          $cm->cache->caps['moodle/site:viewfullnames']  = has_capability('moodle/site:viewfullnames', $modcontext);
3197          $cm->cache->caps['mod/forum:editanypost']      = has_capability('mod/forum:editanypost', $modcontext);
3198          $cm->cache->caps['mod/forum:splitdiscussions'] = has_capability('mod/forum:splitdiscussions', $modcontext);
3199          $cm->cache->caps['mod/forum:deleteownpost']    = has_capability('mod/forum:deleteownpost', $modcontext);
3200          $cm->cache->caps['mod/forum:deleteanypost']    = has_capability('mod/forum:deleteanypost', $modcontext);
3201          $cm->cache->caps['mod/forum:viewanyrating']    = has_capability('mod/forum:viewanyrating', $modcontext);
3202          $cm->cache->caps['mod/forum:exportpost']       = has_capability('mod/forum:exportpost', $modcontext);
3203          $cm->cache->caps['mod/forum:exportownpost']    = has_capability('mod/forum:exportownpost', $modcontext);
3204      }
3205  
3206      if (!isset($cm->uservisible)) {
3207          $cm->uservisible = \core_availability\info_module::is_user_visible($cm, 0, false);
3208      }
3209  
3210      if ($istracked && is_null($postisread)) {
3211          $postisread = forum_tp_is_post_read($USER->id, $post);
3212      }
3213  
3214      if (!forum_user_can_see_post($forum, $discussion, $post, NULL, $cm)) {
3215          $output = '';
3216          if (!$dummyifcantsee) {
3217              if ($return) {
3218                  return $output;
3219              }
3220              echo $output;
3221              return;
3222          }
3223          $output .= html_writer::tag('a', '', array('id'=>'p'.$post->id));
3224          $output .= html_writer::start_tag('div', array('class'=>'forumpost clearfix',
3225                                                         'role' => 'region',
3226                                                         'aria-label' => get_string('hiddenforumpost', 'forum')));
3227          $output .= html_writer::start_tag('div', array('class'=>'row header'));
3228          $output .= html_writer::tag('div', '', array('class'=>'left picture')); // Picture
3229          if ($post->parent) {
3230              $output .= html_writer::start_tag('div', array('class'=>'topic'));
3231          } else {
3232              $output .= html_writer::start_tag('div', array('class'=>'topic starter'));
3233          }
3234          $output .= html_writer::tag('div', get_string('forumsubjecthidden','forum'), array('class' => 'subject',
3235                                                                                             'role' => 'header')); // Subject.
3236          $output .= html_writer::tag('div', get_string('forumauthorhidden', 'forum'), array('class' => 'author',
3237                                                                                             'role' => 'header')); // Author.
3238          $output .= html_writer::end_tag('div');
3239          $output .= html_writer::end_tag('div'); // row
3240          $output .= html_writer::start_tag('div', array('class'=>'row'));
3241          $output .= html_writer::tag('div', '&nbsp;', array('class'=>'left side')); // Groups
3242          $output .= html_writer::tag('div', get_string('forumbodyhidden','forum'), array('class'=>'content')); // Content
3243          $output .= html_writer::end_tag('div'); // row
3244          $output .= html_writer::end_tag('div'); // forumpost
3245  
3246          if ($return) {
3247              return $output;
3248          }
3249          echo $output;
3250          return;
3251      }
3252  
3253      if (empty($str)) {
3254          $str = new stdClass;
3255          $str->edit         = get_string('edit', 'forum');
3256          $str->delete       = get_string('delete', 'forum');
3257          $str->reply        = get_string('reply', 'forum');
3258          $str->parent       = get_string('parent', 'forum');
3259          $str->pruneheading = get_string('pruneheading', 'forum');
3260          $str->prune        = get_string('prune', 'forum');
3261          $str->displaymode     = get_user_preferences('forum_displaymode', $CFG->forum_displaymode);
3262          $str->markread     = get_string('markread', 'forum');
3263          $str->markunread   = get_string('markunread', 'forum');
3264      }
3265  
3266      $discussionlink = new moodle_url('/mod/forum/discuss.php', array('d'=>$post->discussion));
3267  
3268      // Build an object that represents the posting user
3269      $postuser = new stdClass;
3270      $postuserfields = explode(',', user_picture::fields());
3271      $postuser = username_load_fields_from_object($postuser, $post, null, $postuserfields);
3272      $postuser->id = $post->userid;
3273      $postuser->fullname    = fullname($postuser, $cm->cache->caps['moodle/site:viewfullnames']);
3274      $postuser->profilelink = new moodle_url('/user/view.php', array('id'=>$post->userid, 'course'=>$course->id));
3275  
3276      // Prepare the groups the posting user belongs to
3277      if (isset($cm->cache->usersgroups)) {
3278          $groups = array();
3279          if (isset($cm->cache->usersgroups[$post->userid])) {
3280              foreach ($cm->cache->usersgroups[$post->userid] as $gid) {
3281                  $groups[$gid] = $cm->cache->groups[$gid];
3282              }
3283          }
3284      } else {
3285          $groups = groups_get_all_groups($course->id, $post->userid, $cm->groupingid);
3286      }
3287  
3288      // Prepare the attachements for the post, files then images
3289      list($attachments, $attachedimages) = forum_print_attachments($post, $cm, 'separateimages');
3290  
3291      // Determine if we need to shorten this post
3292      $shortenpost = ($link && (strlen(strip_tags($post->message)) > $CFG->forum_longpost));
3293  
3294  
3295      // Prepare an array of commands
3296      $commands = array();
3297  
3298      // SPECIAL CASE: The front page can display a news item post to non-logged in users.
3299      // Don't display the mark read / unread controls in this case.
3300      if ($istracked && $CFG->forum_usermarksread && isloggedin()) {
3301          $url = new moodle_url($discussionlink, array('postid'=>$post->id, 'mark'=>'unread'));
3302          $text = $str->markunread;
3303          if (!$postisread) {
3304              $url->param('mark', 'read');
3305              $text = $str->markread;
3306          }
3307          if ($str->displaymode == FORUM_MODE_THREADED) {
3308              $url->param('parent', $post->parent);
3309          } else {
3310              $url->set_anchor('p'.$post->id);
3311          }
3312          $commands[] = array('url'=>$url, 'text'=>$text);
3313      }
3314  
3315      // Zoom in to the parent specifically
3316      if ($post->parent) {
3317          $url = new moodle_url($discussionlink);
3318          if ($str->displaymode == FORUM_MODE_THREADED) {
3319              $url->param('parent', $post->parent);
3320          } else {
3321              $url->set_anchor('p'.$post->parent);
3322          }
3323          $commands[] = array('url'=>$url, 'text'=>$str->parent);
3324      }
3325  
3326      // Hack for allow to edit news posts those are not displayed yet until they are displayed
3327      $age = time() - $post->created;
3328      if (!$post->parent && $forum->type == 'news' && $discussion->timestart > time()) {
3329          $age = 0;
3330      }
3331  
3332      if ($forum->type == 'single' and $discussion->firstpost == $post->id) {
3333          if (has_capability('moodle/course:manageactivities', $modcontext)) {
3334              // The first post in single simple is the forum description.
3335              $commands[] = array('url'=>new moodle_url('/course/modedit.php', array('update'=>$cm->id, 'sesskey'=>sesskey(), 'return'=>1)), 'text'=>$str->edit);
3336          }
3337      } else if (($ownpost && $age < $CFG->maxeditingtime) || $cm->cache->caps['mod/forum:editanypost']) {
3338          $commands[] = array('url'=>new moodle_url('/mod/forum/post.php', array('edit'=>$post->id)), 'text'=>$str->edit);
3339      }
3340  
3341      if ($cm->cache->caps['mod/forum:splitdiscussions'] && $post->parent && $forum->type != 'single') {
3342          $commands[] = array('url'=>new moodle_url('/mod/forum/post.php', array('prune'=>$post->id)), 'text'=>$str->prune, 'title'=>$str->pruneheading);
3343      }
3344  
3345      if ($forum->type == 'single' and $discussion->firstpost == $post->id) {
3346          // Do not allow deleting of first post in single simple type.
3347      } else if (($ownpost && $age < $CFG->maxeditingtime && $cm->cache->caps['mod/forum:deleteownpost']) || $cm->cache->caps['mod/forum:deleteanypost']) {
3348          $commands[] = array('url'=>new moodle_url('/mod/forum/post.php', array('delete'=>$post->id)), 'text'=>$str->delete);
3349      }
3350  
3351      if ($reply) {
3352          $commands[] = array('url'=>new moodle_url('/mod/forum/post.php#mformforum', array('reply'=>$post->id)), 'text'=>$str->reply);
3353      }
3354  
3355      if ($CFG->enableportfolios && ($cm->cache->caps['mod/forum:exportpost'] || ($ownpost && $cm->cache->caps['mod/forum:exportownpost']))) {
3356          $p = array('postid' => $post->id);
3357          require_once($CFG->libdir.'/portfoliolib.php');
3358          $button = new portfolio_add_button();
3359          $button->set_callback_options('forum_portfolio_caller', array('postid' => $post->id), 'mod_forum');
3360          if (empty($attachments)) {
3361              $button->set_formats(PORTFOLIO_FORMAT_PLAINHTML);
3362          } else {
3363              $button->set_formats(PORTFOLIO_FORMAT_RICHHTML);
3364          }
3365  
3366          $porfoliohtml = $button->to_html(PORTFOLIO_ADD_TEXT_LINK);
3367          if (!empty($porfoliohtml)) {
3368              $commands[] = $porfoliohtml;
3369          }
3370      }
3371      // Finished building commands
3372  
3373  
3374      // Begin output
3375  
3376      $output  = '';
3377  
3378      if ($istracked) {
3379          if ($postisread) {
3380              $forumpostclass = ' read';
3381          } else {
3382              $forumpostclass = ' unread';
3383              $output .= html_writer::tag('a', '', array('name'=>'unread'));
3384          }
3385      } else {
3386          // ignore trackign status if not tracked or tracked param missing
3387          $forumpostclass = '';
3388      }
3389  
3390      $topicclass = '';
3391      if (empty($post->parent)) {
3392          $topicclass = ' firstpost starter';
3393      }
3394  
3395      $postbyuser = new stdClass;
3396      $postbyuser->post = $post->subject;
3397      $postbyuser->user = $postuser->fullname;
3398      $discussionbyuser = get_string('postbyuser', 'forum', $postbyuser);
3399      $output .= html_writer::tag('a', '', array('id'=>'p'.$post->id));
3400      $output .= html_writer::start_tag('div', array('class'=>'forumpost clearfix'.$forumpostclass.$topicclass,
3401                                                     'role' => 'region',
3402                                                     'aria-label' => $discussionbyuser));
3403      $output .= html_writer::start_tag('div', array('class'=>'row header clearfix'));
3404      $output .= html_writer::start_tag('div', array('class'=>'left picture'));
3405      $output .= $OUTPUT->user_picture($postuser, array('courseid'=>$course->id));
3406      $output .= html_writer::end_tag('div');
3407  
3408  
3409      $output .= html_writer::start_tag('div', array('class'=>'topic'.$topicclass));
3410  
3411      $postsubject = $post->subject;
3412      if (empty($post->subjectnoformat)) {
3413          $postsubject = format_string($postsubject);
3414      }
3415      $output .= html_writer::tag('div', $postsubject, array('class'=>'subject',
3416                                                             'role' => 'heading',
3417                                                             'aria-level' => '2'));
3418  
3419      $by = new stdClass();
3420      $by->name = html_writer::link($postuser->profilelink, $postuser->fullname);
3421      $by->date = userdate($post->modified);
3422      $output .= html_writer::tag('div', get_string('bynameondate', 'forum', $by), array('class'=>'author',
3423                                                                                         'role' => 'heading',
3424                                                                                         'aria-level' => '2'));
3425  
3426      $output .= html_writer::end_tag('div'); //topic
3427      $output .= html_writer::end_tag('div'); //row
3428  
3429      $output .= html_writer::start_tag('div', array('class'=>'row maincontent clearfix'));
3430      $output .= html_writer::start_tag('div', array('class'=>'left'));
3431  
3432      $groupoutput = '';
3433      if ($groups) {
3434          $groupoutput = print_group_picture($groups, $course->id, false, true, true);
3435      }
3436      if (empty($groupoutput)) {
3437          $groupoutput = '&nbsp;';
3438      }
3439      $output .= html_writer::tag('div', $groupoutput, array('class'=>'grouppictures'));
3440  
3441      $output .= html_writer::end_tag('div'); //left side
3442      $output .= html_writer::start_tag('div', array('class'=>'no-overflow'));
3443      $output .= html_writer::start_tag('div', array('class'=>'content'));
3444  
3445      $options = new stdClass;
3446      $options->para    = false;
3447      $options->trusted = $post->messagetrust;
3448      $options->context = $modcontext;
3449      if ($shortenpost) {
3450          // Prepare shortened version by filtering the text then shortening it.
3451          $postclass    = 'shortenedpost';
3452          $postcontent  = format_text($post->message, $post->messageformat, $options);
3453          $postcontent  = shorten_text($postcontent, $CFG->forum_shortpost);
3454          $postcontent .= html_writer::link($discussionlink, get_string('readtherest', 'forum'));
3455          $postcontent .= html_writer::tag('div', '('.get_string('numwords', 'moodle', count_words($post->message)).')',
3456              array('class'=>'post-word-count'));
3457      } else {
3458          // Prepare whole post
3459          $postclass    = 'fullpost';
3460          $postcontent  = format_text($post->message, $post->messageformat, $options, $course->id);
3461          if (!empty($highlight)) {
3462              $postcontent = highlight($highlight, $postcontent);
3463          }
3464          if (!empty($forum->displaywordcount)) {
3465              $postcontent .= html_writer::tag('div', get_string('numwords', 'moodle', count_words($post->message)),
3466                  array('class'=>'post-word-count'));
3467          }
3468          $postcontent .= html_writer::tag('div', $attachedimages, array('class'=>'attachedimages'));
3469      }
3470  
3471      // Output the post content
3472      $output .= html_writer::tag('div', $postcontent, array('class'=>'posting '.$postclass));
3473      $output .= html_writer::end_tag('div'); // Content