Merge branch 'MDL-65017-master' of git://github.com/jleyva/moodle
[moodle.git] / mod / forum / classes / local / managers / capability.php
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/>.
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  */
25 namespace mod_forum\local\managers;
27 defined('MOODLE_INTERNAL') || die();
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;
41 require_once($CFG->dirroot . '/mod/forum/lib.php');
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;
65     /**
66      * Constructor.
67      *
68      * @param forum_entity $forum The forum entity to manage capabilities for.
69      * @param legacy_forum_data_mapper $forumdatamapper Legacy forum data mapper
70      * @param legacy_discussion_data_mapper $discussiondatamapper Legacy discussion data mapper
71      * @param legacy_post_data_mapper $postdatamapper Legacy post data mapper
72      */
73     public function __construct(
74         forum_entity $forum,
75         legacy_forum_data_mapper $forumdatamapper,
76         legacy_discussion_data_mapper $discussiondatamapper,
77         legacy_post_data_mapper $postdatamapper
78     ) {
79         $this->forumdatamapper = $forumdatamapper;
80         $this->discussiondatamapper = $discussiondatamapper;
81         $this->postdatamapper = $postdatamapper;
82         $this->forum = $forum;
83         $this->forumrecord = $forumdatamapper->to_legacy_object($forum);
84         $this->context = $forum->get_context();
85     }
87     /**
88      * Can the user subscribe to this forum?
89      *
90      * @param stdClass $user The user to check
91      * @return bool
92      */
93     public function can_subscribe_to_forum(stdClass $user) : bool {
94         if ($this->forum->get_type() == 'single') {
95             return false;
96         }
98         return !is_guest($this->get_context(), $user) &&
99             subscriptions::is_subscribable($this->get_forum_record());
100     }
102     /**
103      * Can the user create discussions in this forum?
104      *
105      * @param stdClass $user The user to check
106      * @param int|null $groupid The current activity group id
107      * @return bool
108      */
109     public function can_create_discussions(stdClass $user, int $groupid = null) : bool {
110         if (isguestuser($user) or !isloggedin()) {
111             return false;
112         }
114         if ($this->forum->is_cutoff_date_reached()) {
115             if (!has_capability('mod/forum:canoverridecutoff', $this->get_context())) {
116                 return false;
117             }
118         }
120         switch ($this->forum->get_type()) {
121             case 'news':
122                 $capability = 'mod/forum:addnews';
123                 break;
124             case 'qanda':
125                 $capability = 'mod/forum:addquestion';
126                 break;
127             default:
128                 $capability = 'mod/forum:startdiscussion';
129         }
131         if (!has_capability($capability, $this->forum->get_context(), $user)) {
132             return false;
133         }
135         if ($this->forum->get_type() == 'eachuser') {
136             if (forum_user_has_posted_discussion($this->forum->get_id(), $user->id, $groupid)) {
137                 return false;
138             }
139         }
141         if ($this->forum->is_in_group_mode()) {
142             return $groupid ? $this->can_access_group($user, $groupid) : $this->can_access_all_groups($user);
143         } else {
144             return true;
145         }
146     }
148     /**
149      * Can the user access all groups?
150      *
151      * @param stdClass $user The user to check
152      * @return bool
153      */
154     public function can_access_all_groups(stdClass $user) : bool {
155         return has_capability('moodle/site:accessallgroups', $this->get_context(), $user);
156     }
158     /**
159      * Can the user access the given group?
160      *
161      * @param stdClass $user The user to check
162      * @param int $groupid The id of the group that the forum is set to
163      * @return bool
164      */
165     public function can_access_group(stdClass $user, int $groupid) : bool {
166         if ($this->can_access_all_groups($user)) {
167             // This user has access to all groups.
168             return true;
169         }
171         // This is a group discussion for a forum in separate groups mode.
172         // Check if the user is a member.
173         // This is the most expensive check.
174         return groups_is_member($groupid, $user->id);
175     }
177     /**
178      * Can the user post to their groups?
179      *
180      * @param stdClass $user The user to check
181      * @return bool
182      */
183     public function can_post_to_my_groups(stdClass $user) : bool {
184         return has_capability('mod/forum:canposttomygroups', $this->get_context(), $user);
185     }
187     /**
188      * Can the user view discussions in this forum?
189      *
190      * @param stdClass $user The user to check
191      * @return bool
192      */
193     public function can_view_discussions(stdClass $user) : bool {
194         return has_capability('mod/forum:viewdiscussion', $this->get_context(), $user);
195     }
197     /**
198      * Can the user move discussions in this forum?
199      *
200      * @param stdClass $user The user to check
201      * @return bool
202      */
203     public function can_move_discussions(stdClass $user) : bool {
204         $forum = $this->get_forum();
205         return $forum->get_type() !== 'single' &&
206                 has_capability('mod/forum:movediscussions', $this->get_context(), $user);
207     }
209     /**
210      * Can the user pin discussions in this forum?
211      *
212      * @param stdClass $user The user to check
213      * @return bool
214      */
215     public function can_pin_discussions(stdClass $user) : bool {
216         return $this->forum->get_type() !== 'single' &&
217                 has_capability('mod/forum:pindiscussions', $this->get_context(), $user);
218     }
220     /**
221      * Can the user split discussions in this forum?
222      *
223      * @param stdClass $user The user to check
224      * @return bool
225      */
226     public function can_split_discussions(stdClass $user) : bool {
227         $forum = $this->get_forum();
228         return $forum->get_type() !== 'single' && has_capability('mod/forum:splitdiscussions', $this->get_context(), $user);
229     }
231     /**
232      * Can the user export (see portfolios) discussions in this forum?
233      *
234      * @param stdClass $user The user to check
235      * @return bool
236      */
237     public function can_export_discussions(stdClass $user) : bool {
238         global $CFG;
239         return $CFG->enableportfolios && has_capability('mod/forum:exportdiscussion', $this->get_context(), $user);
240     }
242     /**
243      * Can the user manually mark posts as read/unread in this forum?
244      *
245      * @param stdClass $user The user to check
246      * @return bool
247      */
248     public function can_manually_control_post_read_status(stdClass $user) : bool {
249         global $CFG;
250         return $CFG->forum_usermarksread && isloggedin() && forum_tp_is_tracked($this->get_forum_record(), $user);
251     }
253     /**
254      * Is the user required to post in the discussion before they can view it?
255      *
256      * @param stdClass $user The user to check
257      * @param discussion_entity $discussion The discussion to check
258      * @return bool
259      */
260     public function must_post_before_viewing_discussion(stdClass $user, discussion_entity $discussion) : bool {
261         $forum = $this->get_forum();
263         if ($forum->get_type() === 'qanda') {
264             // If it's a Q and A forum then the user must either have the capability to view without
265             // posting or the user must have posted before they can view the discussion.
266             return !has_capability('mod/forum:viewqandawithoutposting', $this->get_context(), $user) &&
267                 !forum_user_has_posted($forum->get_id(), $discussion->get_id(), $user->id);
268         } else {
269             // No other forum types require posting before viewing.
270             return false;
271         }
272     }
274     /**
275      * Can the user subscribe to the give discussion?
276      *
277      * @param stdClass $user The user to check
278      * @param discussion_entity $discussion The discussion to check
279      * @return bool
280      */
281     public function can_subscribe_to_discussion(stdClass $user, discussion_entity $discussion) : bool {
282         return $this->can_subscribe_to_forum($user);
283     }
285     /**
286      * Can the user move the discussion in this forum?
287      *
288      * @param stdClass $user The user to check
289      * @param discussion_entity $discussion The discussion to check
290      * @return bool
291      */
292     public function can_move_discussion(stdClass $user, discussion_entity $discussion) : bool {
293         return $this->can_move_discussions($user);
294     }
296     /**
297      * Is the user pin the discussion?
298      *
299      * @param stdClass $user The user to check
300      * @param discussion_entity $discussion The discussion to check
301      * @return bool
302      */
303     public function can_pin_discussion(stdClass $user, discussion_entity $discussion) : bool {
304         return $this->can_pin_discussions($user);
305     }
307     /**
308      * Can the user post in this discussion?
309      *
310      * @param stdClass $user The user to check
311      * @param discussion_entity $discussion The discussion to check
312      * @return bool
313      */
314     public function can_post_in_discussion(stdClass $user, discussion_entity $discussion) : bool {
315         $forum = $this->get_forum();
316         $forumrecord = $this->get_forum_record();
317         $discussionrecord = $this->get_discussion_record($discussion);
318         $context = $this->get_context();
319         $coursemodule = $forum->get_course_module_record();
320         $course = $forum->get_course_record();
322         return forum_user_can_post($forumrecord, $discussionrecord, $user, $coursemodule, $course, $context);
323     }
325     /**
326      * Can the user favourite the discussion
327      *
328      * @param stdClass $user The user to check
329      * @return bool
330      */
331     public function can_favourite_discussion(stdClass $user) : bool {
332         $context = $this->get_context();
333         return has_capability('mod/forum:cantogglefavourite', $context, $user);
334     }
336     /**
337      * Can the user view the content of a discussion?
338      *
339      * @param stdClass $user The user to check
340      * @param discussion_entity $discussion The discussion to check
341      * @return bool
342      */
343     public function can_view_discussion(stdClass $user, discussion_entity $discussion) : bool {
344         $forumrecord = $this->get_forum_record();
345         $discussionrecord = $this->get_discussion_record($discussion);
346         $context = $this->get_context();
348         return forum_user_can_see_discussion($forumrecord, $discussionrecord, $context, $user);
349     }
351     /**
352      * Can the user view the content of the post in this discussion?
353      *
354      * @param stdClass $user The user to check
355      * @param discussion_entity $discussion The discussion to check
356      * @param post_entity $post The post the user wants to view
357      * @return bool
358      */
359     public function can_view_post(stdClass $user, discussion_entity $discussion, post_entity $post) : bool {
360         if (!$this->can_view_post_shell($user, $post)) {
361             return false;
362         }
364         $forum = $this->get_forum();
365         $forumrecord = $this->get_forum_record();
366         $discussionrecord = $this->get_discussion_record($discussion);
367         $postrecord = $this->get_post_record($post);
368         $coursemodule = $forum->get_course_module_record();
369         return forum_user_can_see_post($forumrecord, $discussionrecord, $postrecord, $user, $coursemodule, false);
370     }
372     /**
373      * Can the user view the post at all?
374      * In some situations the user can view the shell of a post without being able to view its content.
375      *
376      * @param   stdClass $user The user to check
377      * @param   post_entity $post The post the user wants to view
378      * @return  bool
379      *
380      */
381     public function can_view_post_shell(stdClass $user, post_entity $post) : bool {
382         if (!$post->is_private_reply()) {
383             return true;
384         }
386         if ($post->is_private_reply_intended_for_user($user)) {
387             return true;
388         }
390         return $this->can_view_any_private_reply($user);
391     }
393     /**
394      * Whether the user can view any private reply in the forum.
395      *
396      * @param   stdClass $user The user to check
397      * @return  bool
398      */
399     public function can_view_any_private_reply(stdClass $user) : bool {
400         return has_capability('mod/forum:readprivatereplies', $this->get_context(), $user);
401     }
403     /**
404      * Can the user edit the post in this discussion?
405      *
406      * @param stdClass $user The user to check
407      * @param discussion_entity $discussion The discussion to check
408      * @param post_entity $post The post the user wants to edit
409      * @return bool
410      */
411     public function can_edit_post(stdClass $user, discussion_entity $discussion, post_entity $post) : bool {
412         global $CFG;
414         $context = $this->get_context();
415         $ownpost = $post->is_owned_by_user($user);
416         $ineditingtime = $post->get_age() < $CFG->maxeditingtime;
418         switch ($this->forum->get_type()) {
419             case 'news':
420                 // Allow editing of news posts once the discussion has started.
421                 $ineditingtime = !$post->has_parent() && $discussion->has_started();
422                 break;
423             case 'single':
424                 if ($discussion->is_first_post($post)) {
425                     return has_capability('moodle/course:manageactivities', $context, $user);
426                 }
427                 break;
428         }
430         return ($ownpost && $ineditingtime) || has_capability('mod/forum:editanypost', $context, $user);
431     }
433     /**
434      * Verifies is the given user can delete a post.
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 delete
439      * @param bool $hasreplies Whether the post has replies
440      * @return bool
441      * @throws moodle_exception
442      */
443     public function validate_delete_post(stdClass $user, discussion_entity $discussion, post_entity $post,
444             bool $hasreplies = false) : void {
445         global $CFG;
447         $forum = $this->get_forum();
449         if ($forum->get_type() == 'single' && $discussion->is_first_post($post)) {
450             // Do not allow deleting of first post in single simple type.
451             throw new moodle_exception('cannotdeletepost', 'forum');
452         }
454         $context = $this->get_context();
455         $ownpost = $post->is_owned_by_user($user);
456         $ineditingtime = $post->get_age() < $CFG->maxeditingtime;
458         if (!($ownpost && $ineditingtime && has_capability('mod/forum:deleteownpost', $context, $user) ||
459                 has_capability('mod/forum:deleteanypost', $context, $user))) {
461             throw new moodle_exception('cannotdeletepost', 'forum');
462         }
464         if ($post->get_total_score()) {
465             throw new moodle_exception('couldnotdeleteratings', 'rating');
466         }
468         if ($hasreplies && !has_capability('mod/forum:deleteanypost', $context, $user)) {
469             throw new moodle_exception('couldnotdeletereplies', 'forum');
470         }
471     }
474     /**
475      * Can the user delete the post in this discussion?
476      *
477      * @param stdClass $user The user to check
478      * @param discussion_entity $discussion The discussion to check
479      * @param post_entity $post The post the user wants to delete
480      * @param bool $hasreplies Whether the post has replies
481      * @return bool
482      */
483     public function can_delete_post(stdClass $user, discussion_entity $discussion, post_entity $post,
484             bool $hasreplies = false) : bool {
486         try {
487             $this->validate_delete_post($user, $discussion, $post, $hasreplies);
488             return true;
489         } catch (moodle_exception $e) {
490             return false;
491         }
492     }
494     /**
495      * Can the user split the post in this discussion?
496      *
497      * @param stdClass $user The user to check
498      * @param discussion_entity $discussion The discussion to check
499      * @param post_entity $post The post the user wants to split
500      * @return bool
501      */
502     public function can_split_post(stdClass $user, discussion_entity $discussion, post_entity $post) : bool {
503         if ($post->is_private_reply()) {
504             // It is not possible to create a private discussion.
505             return false;
506         }
508         return $this->can_split_discussions($user) && $post->has_parent();
509     }
511     /**
512      * Can the user reply to the post in this discussion?
513      *
514      * @param stdClass $user The user to check
515      * @param discussion_entity $discussion The discussion to check
516      * @param post_entity $post The post the user wants to reply to
517      * @return bool
518      */
519     public function can_reply_to_post(stdClass $user, discussion_entity $discussion, post_entity $post) : bool {
520         if ($post->is_private_reply()) {
521             // It is not possible to reply to a private reply.
522             return false;
523         }
525         return $this->can_post_in_discussion($user, $discussion);
526     }
528     /**
529      * Can the user reply privately to the specified post?
530      *
531      * @param stdClass $user The user to check
532      * @param post_entity $post The post the user wants to reply to
533      * @return bool
534      */
535     public function can_reply_privately_to_post(stdClass $user, post_entity $post) : bool {
536         if ($post->is_private_reply()) {
537             // You cannot reply privately to a post which is, itself, a private reply.
538             return false;
539         }
541         return has_capability('mod/forum:postprivatereply', $this->get_context(), $user);
542     }
544     /**
545      * Can the user export (see portfolios) the post in this discussion?
546      *
547      * @param stdClass $user The user to check
548      * @param post_entity $post The post the user wants to export
549      * @return bool
550      */
551     public function can_export_post(stdClass $user, post_entity $post) : bool {
552         global $CFG;
553         $context = $this->get_context();
554         return $CFG->enableportfolios  && (has_capability('mod/forum:exportpost', $context, $user) ||
555             ($post->is_owned_by_user($user) && has_capability('mod/forum:exportownpost', $context, $user)));
556     }
558     /**
559      * Get the forum entity for this capability manager.
560      *
561      * @return forum_entity
562      */
563     protected function get_forum() : forum_entity {
564         return $this->forum;
565     }
567     /**
568      * Get the legacy forum record for this forum.
569      *
570      * @return stdClass
571      */
572     protected function get_forum_record() : stdClass {
573         return $this->forumrecord;
574     }
576     /**
577      * Get the context for this capability manager.
578      *
579      * @return context
580      */
581     protected function get_context() : context {
582         return $this->context;
583     }
585     /**
586      * Get the legacy discussion record for the given discussion entity.
587      *
588      * @param discussion_entity $discussion The discussion to convert
589      * @return stdClass
590      */
591     protected function get_discussion_record(discussion_entity $discussion) : stdClass {
592         return $this->discussiondatamapper->to_legacy_object($discussion);
593     }
595     /**
596      * Get the legacy post record for the given post entity.
597      *
598      * @param post_entity $post The post to convert
599      * @return stdClass
600      */
601     protected function get_post_record(post_entity $post) : stdClass {
602         return $this->postdatamapper->to_legacy_object($post);
603     }
605     /**
606      * Can the user view the participants of this discussion?
607      *
608      * @param stdClass $user The user to check
609      * @param discussion_entity $discussion The discussion to check
610      * @return bool
611      */
612     public function can_view_participants(stdClass $user, discussion_entity $discussion) : bool {
613         return course_can_view_participants($this->get_context()) &&
614             !$this->must_post_before_viewing_discussion($user, $discussion);
615     }
617     /**
618      * Can the user view hidden posts in this forum?
619      *
620      * @param stdClass $user The user to check
621      * @return bool
622      */
623     public function can_view_hidden_posts(stdClass $user) : bool {
624         return has_capability('mod/forum:viewhiddentimedposts', $this->get_context(), $user);
625     }
627     /**
628      * Can the user manage this forum?
629      *
630      * @param stdClass $user The user to check
631      * @return bool
632      */
633     public function can_manage_forum(stdClass $user) {
634         return has_capability('moodle/course:manageactivities', $this->get_context(), $user);
635     }
637     /**
638      * Can the user manage tags on the site?
639      *
640      * @param stdClass $user The user to check
641      * @return bool
642      */
643     public function can_manage_tags(stdClass $user) : bool {
644         return has_capability('moodle/tag:manage', context_system::instance(), $user);
645     }
647     /**
648      * Checks whether the user can self enrol into the course.
649      * Mimics the checks on the add button in deprecatedlib/forum_print_latest_discussions
650      *
651      * @param stdClass $user
652      * @return bool
653      */
654     public function can_self_enrol(stdClass $user) : bool {
655         $canstart = false;
657         if ($this->forum->get_type() != 'news') {
658             if (isguestuser($user) or !isloggedin()) {
659                 $canstart = true;
660             }
662             if (!is_enrolled($this->context) and !is_viewing($this->context)) {
663                  // Allow guests and not-logged-in to see the button - they are prompted to log in after clicking the link,
664                  // Normal users with temporary guest access see this button too, they are asked to enrol instead,
665                  // Do not show the button to users with suspended enrolments here.
666                 $canstart = enrol_selfenrol_available($this->forum->get_course_id());
667             }
668         }
670         return $canstart;
671     }
673     /**
674      * Checks whether the user can export the whole forum (discussions and posts).
675      *
676      * @param stdClass $user The user object.
677      * @return bool True if the user can export the forum or false otherwise.
678      */
679     public function can_export_forum(stdClass $user) : bool {
680         return has_capability('mod/forum:exportforum', $this->get_context(), $user);
681     }