Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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