Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403] [Versions 402 and 403]

   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   * Capability manager for the forum.
  19   *
  20   * @package    mod_forum
  21   * @copyright  2019 Ryan Wyllie <ryan@moodle.com>
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  namespace mod_forum\local\managers;
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  use mod_forum\local\data_mappers\legacy\forum as legacy_forum_data_mapper;
  30  use mod_forum\local\data_mappers\legacy\discussion as legacy_discussion_data_mapper;
  31  use mod_forum\local\data_mappers\legacy\post as legacy_post_data_mapper;
  32  use mod_forum\local\entities\discussion as discussion_entity;
  33  use mod_forum\local\entities\forum as forum_entity;
  34  use mod_forum\local\entities\post as post_entity;
  35  use mod_forum\subscriptions;
  36  use context;
  37  use context_system;
  38  use stdClass;
  39  use moodle_exception;
  40  
  41  require_once($CFG->dirroot . '/mod/forum/lib.php');
  42  
  43  /**
  44   * Capability manager for the forum.
  45   *
  46   * Defines all the business rules for what a user can and can't do in the forum.
  47   *
  48   * @copyright  2019 Ryan Wyllie <ryan@moodle.com>
  49   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  50   */
  51  class capability {
  52      /** @var legacy_forum_data_mapper $forumdatamapper Legacy forum data mapper */
  53      private $forumdatamapper;
  54      /** @var legacy_discussion_data_mapper $discussiondatamapper Legacy discussion data mapper */
  55      private $discussiondatamapper;
  56      /** @var legacy_post_data_mapper $postdatamapper Legacy post data mapper */
  57      private $postdatamapper;
  58      /** @var forum_entity $forum Forum entity */
  59      private $forum;
  60      /** @var stdClass $forumrecord Legacy forum record */
  61      private $forumrecord;
  62      /** @var context $context Module context for the forum */
  63      private $context;
  64      /** @var array $canviewpostcache Cache of discussion posts that can be viewed by a user. */
  65      protected $canviewpostcache = [];
  66  
  67      /**
  68       * Constructor.
  69       *
  70       * @param forum_entity $forum The forum entity to manage capabilities for.
  71       * @param legacy_forum_data_mapper $forumdatamapper Legacy forum data mapper
  72       * @param legacy_discussion_data_mapper $discussiondatamapper Legacy discussion data mapper
  73       * @param legacy_post_data_mapper $postdatamapper Legacy post data mapper
  74       */
  75      public function __construct(
  76          forum_entity $forum,
  77          legacy_forum_data_mapper $forumdatamapper,
  78          legacy_discussion_data_mapper $discussiondatamapper,
  79          legacy_post_data_mapper $postdatamapper
  80      ) {
  81          $this->forumdatamapper = $forumdatamapper;
  82          $this->discussiondatamapper = $discussiondatamapper;
  83          $this->postdatamapper = $postdatamapper;
  84          $this->forum = $forum;
  85          $this->forumrecord = $forumdatamapper->to_legacy_object($forum);
  86          $this->context = $forum->get_context();
  87      }
  88  
  89      /**
  90       * Can the user subscribe to this forum?
  91       *
  92       * @param stdClass $user The user to check
  93       * @return bool
  94       */
  95      public function can_subscribe_to_forum(stdClass $user) : bool {
  96          if ($this->forum->get_type() == 'single') {
  97              return false;
  98          }
  99  
 100          return !is_guest($this->get_context(), $user) &&
 101              subscriptions::is_subscribable($this->get_forum_record());
 102      }
 103  
 104      /**
 105       * Can the user create discussions in this forum?
 106       *
 107       * @param stdClass $user The user to check
 108       * @param int|null $groupid The current activity group id
 109       * @return bool
 110       */
 111      public function can_create_discussions(stdClass $user, int $groupid = null) : bool {
 112          if (isguestuser($user) or !isloggedin()) {
 113              return false;
 114          }
 115  
 116          if ($this->forum->is_cutoff_date_reached()) {
 117              if (!has_capability('mod/forum:canoverridecutoff', $this->get_context())) {
 118                  return false;
 119              }
 120          }
 121  
 122          // If the user reaches the number of posts equal to warning/blocking setting then return the value of canpost in $warningobj.
 123          $cmrecord = $this->forum->get_course_module_record();
 124          if ($warningobj = forum_check_throttling($this->forumrecord, $cmrecord)) {
 125              return $warningobj->canpost;
 126          }
 127  
 128          switch ($this->forum->get_type()) {
 129              case 'news':
 130                  $capability = 'mod/forum:addnews';
 131                  break;
 132              case 'qanda':
 133                  $capability = 'mod/forum:addquestion';
 134                  break;
 135              default:
 136                  $capability = 'mod/forum:startdiscussion';
 137          }
 138  
 139          if (!has_capability($capability, $this->forum->get_context(), $user)) {
 140              return false;
 141          }
 142  
 143          if ($this->forum->get_type() == 'eachuser') {
 144              if (forum_user_has_posted_discussion($this->forum->get_id(), $user->id, $groupid)) {
 145                  return false;
 146              }
 147          }
 148  
 149          if ($this->forum->is_in_group_mode()) {
 150              return $groupid ? $this->can_access_group($user, $groupid) : $this->can_access_all_groups($user);
 151          } else {
 152              return true;
 153          }
 154      }
 155  
 156      /**
 157       * Can the user access all groups?
 158       *
 159       * @param stdClass $user The user to check
 160       * @return bool
 161       */
 162      public function can_access_all_groups(stdClass $user) : bool {
 163          return has_capability('moodle/site:accessallgroups', $this->get_context(), $user);
 164      }
 165  
 166      /**
 167       * Can the user access the given group?
 168       *
 169       * @param stdClass $user The user to check
 170       * @param int $groupid The id of the group that the forum is set to
 171       * @return bool
 172       */
 173      public function can_access_group(stdClass $user, int $groupid) : bool {
 174          if ($this->can_access_all_groups($user)) {
 175              // This user has access to all groups.
 176              return true;
 177          }
 178  
 179          // This is a group discussion for a forum in separate groups mode.
 180          // Check if the user is a member.
 181          // This is the most expensive check.
 182          return groups_is_member($groupid, $user->id);
 183      }
 184  
 185      /**
 186       * Can the user post to their groups?
 187       *
 188       * @param stdClass $user The user to check
 189       * @return bool
 190       */
 191      public function can_post_to_my_groups(stdClass $user) : bool {
 192          return has_capability('mod/forum:canposttomygroups', $this->get_context(), $user);
 193      }
 194  
 195      /**
 196       * Can the user view discussions in this forum?
 197       *
 198       * @param stdClass $user The user to check
 199       * @return bool
 200       */
 201      public function can_view_discussions(stdClass $user) : bool {
 202          return has_capability('mod/forum:viewdiscussion', $this->get_context(), $user);
 203      }
 204  
 205      /**
 206       * Can the user move discussions in this forum?
 207       *
 208       * @param stdClass $user The user to check
 209       * @return bool
 210       */
 211      public function can_move_discussions(stdClass $user) : bool {
 212          $forum = $this->get_forum();
 213          return $forum->get_type() !== 'single' &&
 214                  has_capability('mod/forum:movediscussions', $this->get_context(), $user);
 215      }
 216  
 217      /**
 218       * Can the user pin discussions in this forum?
 219       *
 220       * @param stdClass $user The user to check
 221       * @return bool
 222       */
 223      public function can_pin_discussions(stdClass $user) : bool {
 224          return $this->forum->get_type() !== 'single' &&
 225                  has_capability('mod/forum:pindiscussions', $this->get_context(), $user);
 226      }
 227  
 228      /**
 229       * Can the user split discussions in this forum?
 230       *
 231       * @param stdClass $user The user to check
 232       * @return bool
 233       */
 234      public function can_split_discussions(stdClass $user) : bool {
 235          $forum = $this->get_forum();
 236          return $forum->get_type() !== 'single' && has_capability('mod/forum:splitdiscussions', $this->get_context(), $user);
 237      }
 238  
 239      /**
 240       * Can the user export (see portfolios) discussions in this forum?
 241       *
 242       * @param stdClass $user The user to check
 243       * @return bool
 244       */
 245      public function can_export_discussions(stdClass $user) : bool {
 246          global $CFG;
 247          return $CFG->enableportfolios && has_capability('mod/forum:exportdiscussion', $this->get_context(), $user);
 248      }
 249  
 250      /**
 251       * Can the user manually mark posts as read/unread in this forum?
 252       *
 253       * @param stdClass $user The user to check
 254       * @return bool
 255       */
 256      public function can_manually_control_post_read_status(stdClass $user) : bool {
 257          global $CFG;
 258          return $CFG->forum_usermarksread && isloggedin() && forum_tp_is_tracked($this->get_forum_record(), $user);
 259      }
 260  
 261      /**
 262       * Is the user required to post in the discussion before they can view it?
 263       *
 264       * @param stdClass $user The user to check
 265       * @param discussion_entity $discussion The discussion to check
 266       * @return bool
 267       */
 268      public function must_post_before_viewing_discussion(stdClass $user, discussion_entity $discussion) : bool {
 269          $forum = $this->get_forum();
 270  
 271          if ($forum->get_type() === 'qanda') {
 272              // If it's a Q and A forum then the user must either have the capability to view without
 273              // posting or the user must have posted before they can view the discussion.
 274              return !has_capability('mod/forum:viewqandawithoutposting', $this->get_context(), $user) &&
 275                  !forum_user_has_posted($forum->get_id(), $discussion->get_id(), $user->id);
 276          } else {
 277              // No other forum types require posting before viewing.
 278              return false;
 279          }
 280      }
 281  
 282      /**
 283       * Can the user subscribe to the give discussion?
 284       *
 285       * @param stdClass $user The user to check
 286       * @param discussion_entity $discussion The discussion to check
 287       * @return bool
 288       */
 289      public function can_subscribe_to_discussion(stdClass $user, discussion_entity $discussion) : bool {
 290          return $this->can_subscribe_to_forum($user);
 291      }
 292  
 293      /**
 294       * Can the user move the discussion in this forum?
 295       *
 296       * @param stdClass $user The user to check
 297       * @param discussion_entity $discussion The discussion to check
 298       * @return bool
 299       */
 300      public function can_move_discussion(stdClass $user, discussion_entity $discussion) : bool {
 301          return $this->can_move_discussions($user);
 302      }
 303  
 304      /**
 305       * Is the user pin the discussion?
 306       *
 307       * @param stdClass $user The user to check
 308       * @param discussion_entity $discussion The discussion to check
 309       * @return bool
 310       */
 311      public function can_pin_discussion(stdClass $user, discussion_entity $discussion) : bool {
 312          return $this->can_pin_discussions($user);
 313      }
 314  
 315      /**
 316       * Can the user post in this discussion?
 317       *
 318       * @param stdClass $user The user to check
 319       * @param discussion_entity $discussion The discussion to check
 320       * @return bool
 321       */
 322      public function can_post_in_discussion(stdClass $user, discussion_entity $discussion) : bool {
 323          $forum = $this->get_forum();
 324          $forumrecord = $this->get_forum_record();
 325  
 326          $discussionrecord = $this->get_discussion_record($discussion);
 327          $context = $this->get_context();
 328          $coursemodule = $forum->get_course_module_record();
 329          $course = $forum->get_course_record();
 330  
 331          $status = forum_user_can_post($forumrecord, $discussionrecord, $user, $coursemodule, $course, $context);
 332  
 333          // If the user reaches the number of posts equal to warning/blocking setting then logically and canpost value with $status.
 334          if ($warningobj = forum_check_throttling($forumrecord, $coursemodule)) {
 335              return $status && $warningobj->canpost;
 336          }
 337          return $status;
 338      }
 339  
 340      /**
 341       * Can the user favourite the discussion
 342       *
 343       * @param stdClass $user The user to check
 344       * @return bool
 345       */
 346      public function can_favourite_discussion(stdClass $user) : bool {
 347          $context = $this->get_context();
 348          return has_capability('mod/forum:cantogglefavourite', $context, $user);
 349      }
 350  
 351      /**
 352       * Can the user view the content of a discussion?
 353       *
 354       * @param stdClass $user The user to check
 355       * @param discussion_entity $discussion The discussion to check
 356       * @return bool
 357       */
 358      public function can_view_discussion(stdClass $user, discussion_entity $discussion) : bool {
 359          $forumrecord = $this->get_forum_record();
 360          $discussionrecord = $this->get_discussion_record($discussion);
 361          $context = $this->get_context();
 362  
 363          return forum_user_can_see_discussion($forumrecord, $discussionrecord, $context, $user);
 364      }
 365  
 366      /**
 367       * Can the user view the content of the post in this discussion?
 368       *
 369       * @param stdClass $user The user to check
 370       * @param discussion_entity $discussion The discussion to check
 371       * @param post_entity $post The post the user wants to view
 372       * @return bool
 373       */
 374      public function can_view_post(stdClass $user, discussion_entity $discussion, post_entity $post) : bool {
 375          if (!$this->can_view_post_shell($user, $post)) {
 376              return false;
 377          }
 378  
 379          // Return cached can view if possible.
 380          if (isset($this->canviewpostcache[$user->id][$post->get_id()])) {
 381              return $this->canviewpostcache[$user->id][$post->get_id()];
 382          }
 383  
 384          // Otherwise, check if the user can see this post.
 385          $forum = $this->get_forum();
 386          $forumrecord = $this->get_forum_record();
 387          $discussionrecord = $this->get_discussion_record($discussion);
 388          $postrecord = $this->get_post_record($post);
 389          $coursemodule = $forum->get_course_module_record();
 390          $canviewpost = forum_user_can_see_post($forumrecord, $discussionrecord, $postrecord, $user, $coursemodule, false);
 391  
 392          // Then cache the result before returning.
 393          $this->canviewpostcache[$user->id][$post->get_id()] = $canviewpost;
 394  
 395          return $canviewpost;
 396      }
 397  
 398      /**
 399       * Can the user view the post at all?
 400       * In some situations the user can view the shell of a post without being able to view its content.
 401       *
 402       * @param   stdClass $user The user to check
 403       * @param   post_entity $post The post the user wants to view
 404       * @return  bool
 405       *
 406       */
 407      public function can_view_post_shell(stdClass $user, post_entity $post) : bool {
 408          if ($post->is_owned_by_user($user)) {
 409              return true;
 410          }
 411  
 412          if (!$post->is_private_reply()) {
 413              return true;
 414          }
 415  
 416          if ($post->is_private_reply_intended_for_user($user)) {
 417              return true;
 418          }
 419  
 420          return $this->can_view_any_private_reply($user);
 421      }
 422  
 423      /**
 424       * Whether the user can view any private reply in the forum.
 425       *
 426       * @param   stdClass $user The user to check
 427       * @return  bool
 428       */
 429      public function can_view_any_private_reply(stdClass $user) : bool {
 430          return has_capability('mod/forum:readprivatereplies', $this->get_context(), $user);
 431      }
 432  
 433      /**
 434       * Can the user edit the post in this discussion?
 435       *
 436       * @param stdClass $user The user to check
 437       * @param discussion_entity $discussion The discussion to check
 438       * @param post_entity $post The post the user wants to edit
 439       * @return bool
 440       */
 441      public function can_edit_post(stdClass $user, discussion_entity $discussion, post_entity $post) : bool {
 442          global $CFG;
 443  
 444          $context = $this->get_context();
 445          $ownpost = $post->is_owned_by_user($user);
 446          $ineditingtime = $post->get_age() < $CFG->maxeditingtime;
 447          $mailnow = $post->should_mail_now();
 448  
 449          switch ($this->forum->get_type()) {
 450              case 'news':
 451                  // Allow editing of news posts once the discussion has started.
 452                  $ineditingtime = !$post->has_parent() && $discussion->has_started();
 453                  break;
 454              case 'single':
 455                  if ($discussion->is_first_post($post)) {
 456                      return has_capability('moodle/course:manageactivities', $context, $user);
 457                  }
 458                  break;
 459          }
 460  
 461          return ($ownpost && $ineditingtime && !$mailnow) || has_capability('mod/forum:editanypost', $context, $user);
 462      }
 463  
 464      /**
 465       * Verifies is the given user can delete a post.
 466       *
 467       * @param stdClass $user The user to check
 468       * @param discussion_entity $discussion The discussion to check
 469       * @param post_entity $post The post the user wants to delete
 470       * @param bool $hasreplies Whether the post has replies
 471       * @return bool
 472       * @throws moodle_exception
 473       */
 474      public function validate_delete_post(stdClass $user, discussion_entity $discussion, post_entity $post,
 475              bool $hasreplies = false) : void {
 476          global $CFG;
 477  
 478          $forum = $this->get_forum();
 479  
 480          if ($forum->get_type() == 'single' && $discussion->is_first_post($post)) {
 481              // Do not allow deleting of first post in single simple type.
 482              throw new moodle_exception('cannotdeletepost', 'forum');
 483          }
 484  
 485          $context = $this->get_context();
 486          $ownpost = $post->is_owned_by_user($user);
 487          $ineditingtime = $post->get_age() < $CFG->maxeditingtime;
 488          $mailnow = $post->should_mail_now();
 489  
 490          if (!($ownpost && $ineditingtime && has_capability('mod/forum:deleteownpost', $context, $user) && !$mailnow ||
 491                  has_capability('mod/forum:deleteanypost', $context, $user))) {
 492  
 493              throw new moodle_exception('cannotdeletepost', 'forum');
 494          }
 495  
 496          if ($post->get_total_score()) {
 497              throw new moodle_exception('couldnotdeleteratings', 'rating');
 498          }
 499  
 500          if ($hasreplies && !has_capability('mod/forum:deleteanypost', $context, $user)) {
 501              throw new moodle_exception('couldnotdeletereplies', 'forum');
 502          }
 503      }
 504  
 505  
 506      /**
 507       * Can the user delete the post in this discussion?
 508       *
 509       * @param stdClass $user The user to check
 510       * @param discussion_entity $discussion The discussion to check
 511       * @param post_entity $post The post the user wants to delete
 512       * @param bool $hasreplies Whether the post has replies
 513       * @return bool
 514       */
 515      public function can_delete_post(stdClass $user, discussion_entity $discussion, post_entity $post,
 516              bool $hasreplies = false) : bool {
 517  
 518          try {
 519              $this->validate_delete_post($user, $discussion, $post, $hasreplies);
 520              return true;
 521          } catch (moodle_exception $e) {
 522              return false;
 523          }
 524      }
 525  
 526      /**
 527       * Can the user split the post in this discussion?
 528       *
 529       * @param stdClass $user The user to check
 530       * @param discussion_entity $discussion The discussion to check
 531       * @param post_entity $post The post the user wants to split
 532       * @return bool
 533       */
 534      public function can_split_post(stdClass $user, discussion_entity $discussion, post_entity $post) : bool {
 535          if ($post->is_private_reply()) {
 536              // It is not possible to create a private discussion.
 537              return false;
 538          }
 539  
 540          return $this->can_split_discussions($user) && $post->has_parent();
 541      }
 542  
 543      /**
 544       * Can the user reply to the post in this discussion?
 545       *
 546       * @param stdClass $user The user to check
 547       * @param discussion_entity $discussion The discussion to check
 548       * @param post_entity $post The post the user wants to reply to
 549       * @return bool
 550       */
 551      public function can_reply_to_post(stdClass $user, discussion_entity $discussion, post_entity $post) : bool {
 552          if ($post->is_private_reply()) {
 553              // It is not possible to reply to a private reply.
 554              return false;
 555          } else if (!$this->can_view_post($user, $discussion, $post)) {
 556              // If the user cannot view the post in the first place, the user should not be able to reply to the post.
 557              return false;
 558          }
 559  
 560          return $this->can_post_in_discussion($user, $discussion);
 561      }
 562  
 563      /**
 564       * Can the user reply privately to the specified post?
 565       *
 566       * @param stdClass $user The user to check
 567       * @param post_entity $post The post the user wants to reply to
 568       * @return bool
 569       */
 570      public function can_reply_privately_to_post(stdClass $user, post_entity $post) : bool {
 571          if ($post->is_private_reply()) {
 572              // You cannot reply privately to a post which is, itself, a private reply.
 573              return false;
 574          }
 575  
 576          return has_capability('mod/forum:postprivatereply', $this->get_context(), $user);
 577      }
 578  
 579      /**
 580       * Can the user export (see portfolios) the post in this discussion?
 581       *
 582       * @param stdClass $user The user to check
 583       * @param post_entity $post The post the user wants to export
 584       * @return bool
 585       */
 586      public function can_export_post(stdClass $user, post_entity $post) : bool {
 587          global $CFG;
 588          $context = $this->get_context();
 589          return $CFG->enableportfolios  && (has_capability('mod/forum:exportpost', $context, $user) ||
 590              ($post->is_owned_by_user($user) && has_capability('mod/forum:exportownpost', $context, $user)));
 591      }
 592  
 593      /**
 594       * Get the forum entity for this capability manager.
 595       *
 596       * @return forum_entity
 597       */
 598      protected function get_forum() : forum_entity {
 599          return $this->forum;
 600      }
 601  
 602      /**
 603       * Get the legacy forum record for this forum.
 604       *
 605       * @return stdClass
 606       */
 607      protected function get_forum_record() : stdClass {
 608          return $this->forumrecord;
 609      }
 610  
 611      /**
 612       * Get the context for this capability manager.
 613       *
 614       * @return context
 615       */
 616      protected function get_context() : context {
 617          return $this->context;
 618      }
 619  
 620      /**
 621       * Get the legacy discussion record for the given discussion entity.
 622       *
 623       * @param discussion_entity $discussion The discussion to convert
 624       * @return stdClass
 625       */
 626      protected function get_discussion_record(discussion_entity $discussion) : stdClass {
 627          return $this->discussiondatamapper->to_legacy_object($discussion);
 628      }
 629  
 630      /**
 631       * Get the legacy post record for the given post entity.
 632       *
 633       * @param post_entity $post The post to convert
 634       * @return stdClass
 635       */
 636      protected function get_post_record(post_entity $post) : stdClass {
 637          return $this->postdatamapper->to_legacy_object($post);
 638      }
 639  
 640      /**
 641       * Can the user view the participants of this discussion?
 642       *
 643       * @param stdClass $user The user to check
 644       * @param discussion_entity $discussion The discussion to check
 645       * @return bool
 646       */
 647      public function can_view_participants(stdClass $user, discussion_entity $discussion) : bool {
 648          return course_can_view_participants($this->get_context()) &&
 649              !$this->must_post_before_viewing_discussion($user, $discussion);
 650      }
 651  
 652      /**
 653       * Can the user view hidden posts in this forum?
 654       *
 655       * @param stdClass $user The user to check
 656       * @return bool
 657       */
 658      public function can_view_hidden_posts(stdClass $user) : bool {
 659          return has_capability('mod/forum:viewhiddentimedposts', $this->get_context(), $user);
 660      }
 661  
 662      /**
 663       * Can the user manage this forum?
 664       *
 665       * @param stdClass $user The user to check
 666       * @return bool
 667       */
 668      public function can_manage_forum(stdClass $user) {
 669          return has_capability('moodle/course:manageactivities', $this->get_context(), $user);
 670      }
 671  
 672      /**
 673       * Can the user manage tags on the site?
 674       *
 675       * @param stdClass $user The user to check
 676       * @return bool
 677       */
 678      public function can_manage_tags(stdClass $user) : bool {
 679          return has_capability('moodle/tag:manage', context_system::instance(), $user);
 680      }
 681  
 682      /**
 683       * Checks whether the user can self enrol into the course.
 684       * Mimics the checks on the add button in deprecatedlib/forum_print_latest_discussions
 685       *
 686       * @param stdClass $user
 687       * @return bool
 688       */
 689      public function can_self_enrol(stdClass $user) : bool {
 690          $canstart = false;
 691  
 692          if ($this->forum->get_type() != 'news') {
 693              if (isguestuser($user) or !isloggedin()) {
 694                  $canstart = true;
 695              }
 696  
 697              if (!is_enrolled($this->context) and !is_viewing($this->context)) {
 698                   // Allow guests and not-logged-in to see the button - they are prompted to log in after clicking the link,
 699                   // Normal users with temporary guest access see this button too, they are asked to enrol instead,
 700                   // Do not show the button to users with suspended enrolments here.
 701                  $canstart = enrol_selfenrol_available($this->forum->get_course_id());
 702              }
 703          }
 704  
 705          return $canstart;
 706      }
 707  
 708      /**
 709       * Checks whether the user can export the whole forum (discussions and posts).
 710       *
 711       * @param stdClass $user The user object.
 712       * @return bool True if the user can export the forum or false otherwise.
 713       */
 714      public function can_export_forum(stdClass $user) : bool {
 715          return has_capability('mod/forum:exportforum', $this->get_context(), $user);
 716      }
 717  
 718      /**
 719       * Check whether the supplied grader can grade the gradee.
 720       *
 721       * @param stdClass $grader The user grading
 722       * @param stdClass $gradee The user being graded
 723       * @return bool
 724       */
 725      public function can_grade(stdClass $grader, stdClass $gradee = null): bool {
 726          if (!has_capability('mod/forum:grade', $this->get_context(), $grader)) {
 727              return false;
 728          }
 729  
 730          return true;
 731      }
 732  }