See Release Notes
Long Term Support Release
<?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Capability manager for the forum. * * @package mod_forum * @copyright 2019 Ryan Wyllie <ryan@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\managers; defined('MOODLE_INTERNAL') || die(); use mod_forum\local\data_mappers\legacy\forum as legacy_forum_data_mapper; use mod_forum\local\data_mappers\legacy\discussion as legacy_discussion_data_mapper; use mod_forum\local\data_mappers\legacy\post as legacy_post_data_mapper; use mod_forum\local\entities\discussion as discussion_entity; use mod_forum\local\entities\forum as forum_entity; use mod_forum\local\entities\post as post_entity; use mod_forum\subscriptions; use context; use context_system; use stdClass; use moodle_exception; require_once($CFG->dirroot . '/mod/forum/lib.php'); /** * Capability manager for the forum. * * Defines all the business rules for what a user can and can't do in the forum. * * @copyright 2019 Ryan Wyllie <ryan@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class capability { /** @var legacy_forum_data_mapper $forumdatamapper Legacy forum data mapper */ private $forumdatamapper; /** @var legacy_discussion_data_mapper $discussiondatamapper Legacy discussion data mapper */ private $discussiondatamapper; /** @var legacy_post_data_mapper $postdatamapper Legacy post data mapper */ private $postdatamapper; /** @var forum_entity $forum Forum entity */ private $forum; /** @var stdClass $forumrecord Legacy forum record */ private $forumrecord; /** @var context $context Module context for the forum */ private $context; /** @var array $canviewpostcache Cache of discussion posts that can be viewed by a user. */ protected $canviewpostcache = []; /** * Constructor. * * @param forum_entity $forum The forum entity to manage capabilities for. * @param legacy_forum_data_mapper $forumdatamapper Legacy forum data mapper * @param legacy_discussion_data_mapper $discussiondatamapper Legacy discussion data mapper * @param legacy_post_data_mapper $postdatamapper Legacy post data mapper */ public function __construct( forum_entity $forum, legacy_forum_data_mapper $forumdatamapper, legacy_discussion_data_mapper $discussiondatamapper, legacy_post_data_mapper $postdatamapper ) { $this->forumdatamapper = $forumdatamapper; $this->discussiondatamapper = $discussiondatamapper; $this->postdatamapper = $postdatamapper; $this->forum = $forum; $this->forumrecord = $forumdatamapper->to_legacy_object($forum); $this->context = $forum->get_context(); } /** * Can the user subscribe to this forum? * * @param stdClass $user The user to check * @return bool */ public function can_subscribe_to_forum(stdClass $user) : bool { if ($this->forum->get_type() == 'single') { return false; } return !is_guest($this->get_context(), $user) && subscriptions::is_subscribable($this->get_forum_record()); } /** * Can the user create discussions in this forum? * * @param stdClass $user The user to check * @param int|null $groupid The current activity group id * @return bool */ public function can_create_discussions(stdClass $user, int $groupid = null) : bool { if (isguestuser($user) or !isloggedin()) { return false; } if ($this->forum->is_cutoff_date_reached()) { if (!has_capability('mod/forum:canoverridecutoff', $this->get_context())) { return false; } } // If the user reaches the number of posts equal to warning/blocking setting then return the value of canpost in $warningobj. $cmrecord = $this->forum->get_course_module_record(); if ($warningobj = forum_check_throttling($this->forumrecord, $cmrecord)) { return $warningobj->canpost; } switch ($this->forum->get_type()) { case 'news': $capability = 'mod/forum:addnews'; break; case 'qanda': $capability = 'mod/forum:addquestion'; break; default: $capability = 'mod/forum:startdiscussion'; } if (!has_capability($capability, $this->forum->get_context(), $user)) { return false; } if ($this->forum->get_type() == 'eachuser') { if (forum_user_has_posted_discussion($this->forum->get_id(), $user->id, $groupid)) { return false; } } if ($this->forum->is_in_group_mode()) { return $groupid ? $this->can_access_group($user, $groupid) : $this->can_access_all_groups($user); } else { return true; } } /** * Can the user access all groups? * * @param stdClass $user The user to check * @return bool */ public function can_access_all_groups(stdClass $user) : bool { return has_capability('moodle/site:accessallgroups', $this->get_context(), $user); } /** * Can the user access the given group? * * @param stdClass $user The user to check * @param int $groupid The id of the group that the forum is set to * @return bool */ public function can_access_group(stdClass $user, int $groupid) : bool { if ($this->can_access_all_groups($user)) { // This user has access to all groups. return true; } // This is a group discussion for a forum in separate groups mode. // Check if the user is a member. // This is the most expensive check. return groups_is_member($groupid, $user->id); } /** * Can the user post to their groups? * * @param stdClass $user The user to check * @return bool */ public function can_post_to_my_groups(stdClass $user) : bool { return has_capability('mod/forum:canposttomygroups', $this->get_context(), $user); } /** * Can the user view discussions in this forum? * * @param stdClass $user The user to check * @return bool */ public function can_view_discussions(stdClass $user) : bool { return has_capability('mod/forum:viewdiscussion', $this->get_context(), $user); } /** * Can the user move discussions in this forum? * * @param stdClass $user The user to check * @return bool */ public function can_move_discussions(stdClass $user) : bool { $forum = $this->get_forum(); return $forum->get_type() !== 'single' && has_capability('mod/forum:movediscussions', $this->get_context(), $user); } /** * Can the user pin discussions in this forum? * * @param stdClass $user The user to check * @return bool */ public function can_pin_discussions(stdClass $user) : bool { return $this->forum->get_type() !== 'single' && has_capability('mod/forum:pindiscussions', $this->get_context(), $user); } /** * Can the user split discussions in this forum? * * @param stdClass $user The user to check * @return bool */ public function can_split_discussions(stdClass $user) : bool { $forum = $this->get_forum(); return $forum->get_type() !== 'single' && has_capability('mod/forum:splitdiscussions', $this->get_context(), $user); } /** * Can the user export (see portfolios) discussions in this forum? * * @param stdClass $user The user to check * @return bool */ public function can_export_discussions(stdClass $user) : bool { global $CFG; return $CFG->enableportfolios && has_capability('mod/forum:exportdiscussion', $this->get_context(), $user); } /** * Can the user manually mark posts as read/unread in this forum? * * @param stdClass $user The user to check * @return bool */ public function can_manually_control_post_read_status(stdClass $user) : bool { global $CFG; return $CFG->forum_usermarksread && isloggedin() && forum_tp_is_tracked($this->get_forum_record(), $user); } /** * Is the user required to post in the discussion before they can view it? * * @param stdClass $user The user to check * @param discussion_entity $discussion The discussion to check * @return bool */ public function must_post_before_viewing_discussion(stdClass $user, discussion_entity $discussion) : bool { $forum = $this->get_forum(); if ($forum->get_type() === 'qanda') { // If it's a Q and A forum then the user must either have the capability to view without // posting or the user must have posted before they can view the discussion. return !has_capability('mod/forum:viewqandawithoutposting', $this->get_context(), $user) && !forum_user_has_posted($forum->get_id(), $discussion->get_id(), $user->id); } else { // No other forum types require posting before viewing. return false; } } /** * Can the user subscribe to the give discussion? * * @param stdClass $user The user to check * @param discussion_entity $discussion The discussion to check * @return bool */ public function can_subscribe_to_discussion(stdClass $user, discussion_entity $discussion) : bool { return $this->can_subscribe_to_forum($user); } /** * Can the user move the discussion in this forum? * * @param stdClass $user The user to check * @param discussion_entity $discussion The discussion to check * @return bool */ public function can_move_discussion(stdClass $user, discussion_entity $discussion) : bool { return $this->can_move_discussions($user); } /** * Is the user pin the discussion? * * @param stdClass $user The user to check * @param discussion_entity $discussion The discussion to check * @return bool */ public function can_pin_discussion(stdClass $user, discussion_entity $discussion) : bool { return $this->can_pin_discussions($user); } /** * Can the user post in this discussion? * * @param stdClass $user The user to check * @param discussion_entity $discussion The discussion to check * @return bool */ public function can_post_in_discussion(stdClass $user, discussion_entity $discussion) : bool { $forum = $this->get_forum(); $forumrecord = $this->get_forum_record(); $discussionrecord = $this->get_discussion_record($discussion); $context = $this->get_context(); $coursemodule = $forum->get_course_module_record(); $course = $forum->get_course_record(); $status = forum_user_can_post($forumrecord, $discussionrecord, $user, $coursemodule, $course, $context); // If the user reaches the number of posts equal to warning/blocking setting then logically and canpost value with $status. if ($warningobj = forum_check_throttling($forumrecord, $coursemodule)) { return $status && $warningobj->canpost; } return $status; } /** * Can the user favourite the discussion * * @param stdClass $user The user to check * @return bool */ public function can_favourite_discussion(stdClass $user) : bool { $context = $this->get_context(); return has_capability('mod/forum:cantogglefavourite', $context, $user); } /** * Can the user view the content of a discussion? * * @param stdClass $user The user to check * @param discussion_entity $discussion The discussion to check * @return bool */ public function can_view_discussion(stdClass $user, discussion_entity $discussion) : bool { $forumrecord = $this->get_forum_record(); $discussionrecord = $this->get_discussion_record($discussion); $context = $this->get_context(); return forum_user_can_see_discussion($forumrecord, $discussionrecord, $context, $user); } /** * Can the user view the content of the post in this discussion? * * @param stdClass $user The user to check * @param discussion_entity $discussion The discussion to check * @param post_entity $post The post the user wants to view * @return bool */ public function can_view_post(stdClass $user, discussion_entity $discussion, post_entity $post) : bool { if (!$this->can_view_post_shell($user, $post)) { return false; } // Return cached can view if possible. if (isset($this->canviewpostcache[$user->id][$post->get_id()])) { return $this->canviewpostcache[$user->id][$post->get_id()]; } // Otherwise, check if the user can see this post. $forum = $this->get_forum(); $forumrecord = $this->get_forum_record(); $discussionrecord = $this->get_discussion_record($discussion); $postrecord = $this->get_post_record($post); $coursemodule = $forum->get_course_module_record(); $canviewpost = forum_user_can_see_post($forumrecord, $discussionrecord, $postrecord, $user, $coursemodule, false); // Then cache the result before returning. $this->canviewpostcache[$user->id][$post->get_id()] = $canviewpost; return $canviewpost; } /** * Can the user view the post at all? * In some situations the user can view the shell of a post without being able to view its content. * * @param stdClass $user The user to check * @param post_entity $post The post the user wants to view * @return bool * */ public function can_view_post_shell(stdClass $user, post_entity $post) : bool { if ($post->is_owned_by_user($user)) { return true; } if (!$post->is_private_reply()) { return true; } if ($post->is_private_reply_intended_for_user($user)) { return true; } return $this->can_view_any_private_reply($user); } /** * Whether the user can view any private reply in the forum. * * @param stdClass $user The user to check * @return bool */ public function can_view_any_private_reply(stdClass $user) : bool { return has_capability('mod/forum:readprivatereplies', $this->get_context(), $user); } /** * Can the user edit the post in this discussion? * * @param stdClass $user The user to check * @param discussion_entity $discussion The discussion to check * @param post_entity $post The post the user wants to edit * @return bool */ public function can_edit_post(stdClass $user, discussion_entity $discussion, post_entity $post) : bool { global $CFG; $context = $this->get_context(); $ownpost = $post->is_owned_by_user($user); $ineditingtime = $post->get_age() < $CFG->maxeditingtime;> $mailnow = $post->should_mail_now();switch ($this->forum->get_type()) { case 'news': // Allow editing of news posts once the discussion has started. $ineditingtime = !$post->has_parent() && $discussion->has_started(); break; case 'single': if ($discussion->is_first_post($post)) { return has_capability('moodle/course:manageactivities', $context, $user); } break; }< return ($ownpost && $ineditingtime) || has_capability('mod/forum:editanypost', $context, $user);> return ($ownpost && $ineditingtime && !$mailnow) || has_capability('mod/forum:editanypost', $context, $user);} /** * Verifies is the given user can delete a post. * * @param stdClass $user The user to check * @param discussion_entity $discussion The discussion to check * @param post_entity $post The post the user wants to delete * @param bool $hasreplies Whether the post has replies * @return bool * @throws moodle_exception */ public function validate_delete_post(stdClass $user, discussion_entity $discussion, post_entity $post, bool $hasreplies = false) : void { global $CFG; $forum = $this->get_forum(); if ($forum->get_type() == 'single' && $discussion->is_first_post($post)) { // Do not allow deleting of first post in single simple type. throw new moodle_exception('cannotdeletepost', 'forum'); } $context = $this->get_context(); $ownpost = $post->is_owned_by_user($user); $ineditingtime = $post->get_age() < $CFG->maxeditingtime;> $mailnow = $post->should_mail_now();< if (!($ownpost && $ineditingtime && has_capability('mod/forum:deleteownpost', $context, $user) ||> if (!($ownpost && $ineditingtime && has_capability('mod/forum:deleteownpost', $context, $user) && !$mailnow ||has_capability('mod/forum:deleteanypost', $context, $user))) { throw new moodle_exception('cannotdeletepost', 'forum'); } if ($post->get_total_score()) { throw new moodle_exception('couldnotdeleteratings', 'rating'); } if ($hasreplies && !has_capability('mod/forum:deleteanypost', $context, $user)) { throw new moodle_exception('couldnotdeletereplies', 'forum'); } } /** * Can the user delete the post in this discussion? * * @param stdClass $user The user to check * @param discussion_entity $discussion The discussion to check * @param post_entity $post The post the user wants to delete * @param bool $hasreplies Whether the post has replies * @return bool */ public function can_delete_post(stdClass $user, discussion_entity $discussion, post_entity $post, bool $hasreplies = false) : bool { try { $this->validate_delete_post($user, $discussion, $post, $hasreplies); return true; } catch (moodle_exception $e) { return false; } } /** * Can the user split the post in this discussion? * * @param stdClass $user The user to check * @param discussion_entity $discussion The discussion to check * @param post_entity $post The post the user wants to split * @return bool */ public function can_split_post(stdClass $user, discussion_entity $discussion, post_entity $post) : bool { if ($post->is_private_reply()) { // It is not possible to create a private discussion. return false; } return $this->can_split_discussions($user) && $post->has_parent(); } /** * Can the user reply to the post in this discussion? * * @param stdClass $user The user to check * @param discussion_entity $discussion The discussion to check * @param post_entity $post The post the user wants to reply to * @return bool */ public function can_reply_to_post(stdClass $user, discussion_entity $discussion, post_entity $post) : bool { if ($post->is_private_reply()) { // It is not possible to reply to a private reply. return false; } else if (!$this->can_view_post($user, $discussion, $post)) { // If the user cannot view the post in the first place, the user should not be able to reply to the post. return false; } return $this->can_post_in_discussion($user, $discussion); } /** * Can the user reply privately to the specified post? * * @param stdClass $user The user to check * @param post_entity $post The post the user wants to reply to * @return bool */ public function can_reply_privately_to_post(stdClass $user, post_entity $post) : bool { if ($post->is_private_reply()) { // You cannot reply privately to a post which is, itself, a private reply. return false; } return has_capability('mod/forum:postprivatereply', $this->get_context(), $user); } /** * Can the user export (see portfolios) the post in this discussion? * * @param stdClass $user The user to check * @param post_entity $post The post the user wants to export * @return bool */ public function can_export_post(stdClass $user, post_entity $post) : bool { global $CFG; $context = $this->get_context(); return $CFG->enableportfolios && (has_capability('mod/forum:exportpost', $context, $user) || ($post->is_owned_by_user($user) && has_capability('mod/forum:exportownpost', $context, $user))); } /** * Get the forum entity for this capability manager. * * @return forum_entity */ protected function get_forum() : forum_entity { return $this->forum; } /** * Get the legacy forum record for this forum. * * @return stdClass */ protected function get_forum_record() : stdClass { return $this->forumrecord; } /** * Get the context for this capability manager. * * @return context */ protected function get_context() : context { return $this->context; } /** * Get the legacy discussion record for the given discussion entity. * * @param discussion_entity $discussion The discussion to convert * @return stdClass */ protected function get_discussion_record(discussion_entity $discussion) : stdClass { return $this->discussiondatamapper->to_legacy_object($discussion); } /** * Get the legacy post record for the given post entity. * * @param post_entity $post The post to convert * @return stdClass */ protected function get_post_record(post_entity $post) : stdClass { return $this->postdatamapper->to_legacy_object($post); } /** * Can the user view the participants of this discussion? * * @param stdClass $user The user to check * @param discussion_entity $discussion The discussion to check * @return bool */ public function can_view_participants(stdClass $user, discussion_entity $discussion) : bool { return course_can_view_participants($this->get_context()) && !$this->must_post_before_viewing_discussion($user, $discussion); } /** * Can the user view hidden posts in this forum? * * @param stdClass $user The user to check * @return bool */ public function can_view_hidden_posts(stdClass $user) : bool { return has_capability('mod/forum:viewhiddentimedposts', $this->get_context(), $user); } /** * Can the user manage this forum? * * @param stdClass $user The user to check * @return bool */ public function can_manage_forum(stdClass $user) { return has_capability('moodle/course:manageactivities', $this->get_context(), $user); } /** * Can the user manage tags on the site? * * @param stdClass $user The user to check * @return bool */ public function can_manage_tags(stdClass $user) : bool { return has_capability('moodle/tag:manage', context_system::instance(), $user); } /** * Checks whether the user can self enrol into the course. * Mimics the checks on the add button in deprecatedlib/forum_print_latest_discussions * * @param stdClass $user * @return bool */ public function can_self_enrol(stdClass $user) : bool { $canstart = false; if ($this->forum->get_type() != 'news') { if (isguestuser($user) or !isloggedin()) { $canstart = true; } if (!is_enrolled($this->context) and !is_viewing($this->context)) { // Allow guests and not-logged-in to see the button - they are prompted to log in after clicking the link, // Normal users with temporary guest access see this button too, they are asked to enrol instead, // Do not show the button to users with suspended enrolments here. $canstart = enrol_selfenrol_available($this->forum->get_course_id()); } } return $canstart; } /** * Checks whether the user can export the whole forum (discussions and posts). * * @param stdClass $user The user object. * @return bool True if the user can export the forum or false otherwise. */ public function can_export_forum(stdClass $user) : bool { return has_capability('mod/forum:exportforum', $this->get_context(), $user); } /** * Check whether the supplied grader can grade the gradee. * * @param stdClass $grader The user grading * @param stdClass $gradee The user being graded * @return bool */ public function can_grade(stdClass $grader, stdClass $gradee = null): bool { if (!has_capability('mod/forum:grade', $this->get_context(), $grader)) { return false; } return true; } }