Merge branch 'MDL-66927-run-new-adhoc' of https://github.com/brendanheywood/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;
40 require_once($CFG->dirroot . '/mod/forum/lib.php');
42 /**
43  * Capability manager for the forum.
44  *
45  * Defines all the business rules for what a user can and can't do in the forum.
46  *
47  * @copyright  2019 Ryan Wyllie <ryan@moodle.com>
48  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
49  */
50 class capability {
51     /** @var legacy_forum_data_mapper $forumdatamapper Legacy forum data mapper */
52     private $forumdatamapper;
53     /** @var legacy_discussion_data_mapper $discussiondatamapper Legacy discussion data mapper */
54     private $discussiondatamapper;
55     /** @var legacy_post_data_mapper $postdatamapper Legacy post data mapper */
56     private $postdatamapper;
57     /** @var forum_entity $forum Forum entity */
58     private $forum;
59     /** @var stdClass $forumrecord Legacy forum record */
60     private $forumrecord;
61     /** @var context $context Module context for the forum */
62     private $context;
64     /**
65      * Constructor.
66      *
67      * @param forum_entity $forum The forum entity to manage capabilities for.
68      * @param legacy_forum_data_mapper $forumdatamapper Legacy forum data mapper
69      * @param legacy_discussion_data_mapper $discussiondatamapper Legacy discussion data mapper
70      * @param legacy_post_data_mapper $postdatamapper Legacy post data mapper
71      */
72     public function __construct(
73         forum_entity $forum,
74         legacy_forum_data_mapper $forumdatamapper,
75         legacy_discussion_data_mapper $discussiondatamapper,
76         legacy_post_data_mapper $postdatamapper
77     ) {
78         $this->forumdatamapper = $forumdatamapper;
79         $this->discussiondatamapper = $discussiondatamapper;
80         $this->postdatamapper = $postdatamapper;
81         $this->forum = $forum;
82         $this->forumrecord = $forumdatamapper->to_legacy_object($forum);
83         $this->context = $forum->get_context();
84     }
86     /**
87      * Can the user subscribe to this forum?
88      *
89      * @param stdClass $user The user to check
90      * @return bool
91      */
92     public function can_subscribe_to_forum(stdClass $user) : bool {
93         if ($this->forum->get_type() == 'single') {
94             return false;
95         }
97         return !is_guest($this->get_context(), $user) &&
98             subscriptions::is_subscribable($this->get_forum_record());
99     }
101     /**
102      * Can the user create discussions in this forum?
103      *
104      * @param stdClass $user The user to check
105      * @param int|null $groupid The current activity group id
106      * @return bool
107      */
108     public function can_create_discussions(stdClass $user, int $groupid = null) : bool {
109         if (isguestuser($user) or !isloggedin()) {
110             return false;
111         }
113         if ($this->forum->is_cutoff_date_reached()) {
114             if (!has_capability('mod/forum:canoverridecutoff', $this->get_context())) {
115                 return false;
116             }
117         }
119         switch ($this->forum->get_type()) {
120             case 'news':
121                 $capability = 'mod/forum:addnews';
122                 break;
123             case 'qanda':
124                 $capability = 'mod/forum:addquestion';
125                 break;
126             default:
127                 $capability = 'mod/forum:startdiscussion';
128         }
130         if (!has_capability($capability, $this->forum->get_context(), $user)) {
131             return false;
132         }
134         if ($this->forum->get_type() == 'eachuser') {
135             if (forum_user_has_posted_discussion($this->forum->get_id(), $user->id, $groupid)) {
136                 return false;
137             }
138         }
140         if ($this->forum->is_in_group_mode()) {
141             return $groupid ? $this->can_access_group($user, $groupid) : $this->can_access_all_groups($user);
142         } else {
143             return true;
144         }
145     }
147     /**
148      * Can the user access all groups?
149      *
150      * @param stdClass $user The user to check
151      * @return bool
152      */
153     public function can_access_all_groups(stdClass $user) : bool {
154         return has_capability('moodle/site:accessallgroups', $this->get_context(), $user);
155     }
157     /**
158      * Can the user access the given group?
159      *
160      * @param stdClass $user The user to check
161      * @param int $groupid The id of the group that the forum is set to
162      * @return bool
163      */
164     public function can_access_group(stdClass $user, int $groupid) : bool {
165         if ($this->can_access_all_groups($user)) {
166             // This user has access to all groups.
167             return true;
168         }
170         // This is a group discussion for a forum in separate groups mode.
171         // Check if the user is a member.
172         // This is the most expensive check.
173         return groups_is_member($groupid, $user->id);
174     }
176     /**
177      * Can the user post to their groups?
178      *
179      * @param stdClass $user The user to check
180      * @return bool
181      */
182     public function can_post_to_my_groups(stdClass $user) : bool {
183         return has_capability('mod/forum:canposttomygroups', $this->get_context(), $user);
184     }
186     /**
187      * Can the user view discussions in this forum?
188      *
189      * @param stdClass $user The user to check
190      * @return bool
191      */
192     public function can_view_discussions(stdClass $user) : bool {
193         return has_capability('mod/forum:viewdiscussion', $this->get_context(), $user);
194     }
196     /**
197      * Can the user move discussions in this forum?
198      *
199      * @param stdClass $user The user to check
200      * @return bool
201      */
202     public function can_move_discussions(stdClass $user) : bool {
203         $forum = $this->get_forum();
204         return $forum->get_type() !== 'single' &&
205                 has_capability('mod/forum:movediscussions', $this->get_context(), $user);
206     }
208     /**
209      * Can the user pin discussions in this forum?
210      *
211      * @param stdClass $user The user to check
212      * @return bool
213      */
214     public function can_pin_discussions(stdClass $user) : bool {
215         return $this->forum->get_type() !== 'single' &&
216                 has_capability('mod/forum:pindiscussions', $this->get_context(), $user);
217     }
219     /**
220      * Can the user split discussions in this forum?
221      *
222      * @param stdClass $user The user to check
223      * @return bool
224      */
225     public function can_split_discussions(stdClass $user) : bool {
226         $forum = $this->get_forum();
227         return $forum->get_type() !== 'single' && has_capability('mod/forum:splitdiscussions', $this->get_context(), $user);
228     }
230     /**
231      * Can the user export (see portfolios) discussions in this forum?
232      *
233      * @param stdClass $user The user to check
234      * @return bool
235      */
236     public function can_export_discussions(stdClass $user) : bool {
237         global $CFG;
238         return $CFG->enableportfolios && has_capability('mod/forum:exportdiscussion', $this->get_context(), $user);
239     }
241     /**
242      * Can the user manually mark posts as read/unread in this forum?
243      *
244      * @param stdClass $user The user to check
245      * @return bool
246      */
247     public function can_manually_control_post_read_status(stdClass $user) : bool {
248         global $CFG;
249         return $CFG->forum_usermarksread && isloggedin() && forum_tp_is_tracked($this->get_forum_record(), $user);
250     }
252     /**
253      * Is the user required to post in the discussion before they can view it?
254      *
255      * @param stdClass $user The user to check
256      * @param discussion_entity $discussion The discussion to check
257      * @return bool
258      */
259     public function must_post_before_viewing_discussion(stdClass $user, discussion_entity $discussion) : bool {
260         $forum = $this->get_forum();
262         if ($forum->get_type() === 'qanda') {
263             // If it's a Q and A forum then the user must either have the capability to view without
264             // posting or the user must have posted before they can view the discussion.
265             return !has_capability('mod/forum:viewqandawithoutposting', $this->get_context(), $user) &&
266                 !forum_user_has_posted($forum->get_id(), $discussion->get_id(), $user->id);
267         } else {
268             // No other forum types require posting before viewing.
269             return false;
270         }
271     }
273     /**
274      * Can the user subscribe to the give discussion?
275      *
276      * @param stdClass $user The user to check
277      * @param discussion_entity $discussion The discussion to check
278      * @return bool
279      */
280     public function can_subscribe_to_discussion(stdClass $user, discussion_entity $discussion) : bool {
281         return $this->can_subscribe_to_forum($user);
282     }
284     /**
285      * Can the user move the discussion in this forum?
286      *
287      * @param stdClass $user The user to check
288      * @param discussion_entity $discussion The discussion to check
289      * @return bool
290      */
291     public function can_move_discussion(stdClass $user, discussion_entity $discussion) : bool {
292         return $this->can_move_discussions($user);
293     }
295     /**
296      * Is the user pin the discussion?
297      *
298      * @param stdClass $user The user to check
299      * @param discussion_entity $discussion The discussion to check
300      * @return bool
301      */
302     public function can_pin_discussion(stdClass $user, discussion_entity $discussion) : bool {
303         return $this->can_pin_discussions($user);
304     }
306     /**
307      * Can the user post in this discussion?
308      *
309      * @param stdClass $user The user to check
310      * @param discussion_entity $discussion The discussion to check
311      * @return bool
312      */
313     public function can_post_in_discussion(stdClass $user, discussion_entity $discussion) : bool {
314         $forum = $this->get_forum();
315         $forumrecord = $this->get_forum_record();
316         $discussionrecord = $this->get_discussion_record($discussion);
317         $context = $this->get_context();
318         $coursemodule = $forum->get_course_module_record();
319         $course = $forum->get_course_record();
321         return forum_user_can_post($forumrecord, $discussionrecord, $user, $coursemodule, $course, $context);
322     }
324     /**
325      * Can the user favourite the discussion
326      *
327      * @param stdClass $user The user to check
328      * @return bool
329      */
330     public function can_favourite_discussion(stdClass $user) : bool {
331         $context = $this->get_context();
332         return has_capability('mod/forum:cantogglefavourite', $context, $user);
333     }
335     /**
336      * Can the user view the content of a discussion?
337      *
338      * @param stdClass $user The user to check
339      * @param discussion_entity $discussion The discussion to check
340      * @return bool
341      */
342     public function can_view_discussion(stdClass $user, discussion_entity $discussion) : bool {
343         $forumrecord = $this->get_forum_record();
344         $discussionrecord = $this->get_discussion_record($discussion);
345         $context = $this->get_context();
347         return forum_user_can_see_discussion($forumrecord, $discussionrecord, $context, $user);
348     }
350     /**
351      * Can the user view the content of the post in this discussion?
352      *
353      * @param stdClass $user The user to check
354      * @param discussion_entity $discussion The discussion to check
355      * @param post_entity $post The post the user wants to view
356      * @return bool
357      */
358     public function can_view_post(stdClass $user, discussion_entity $discussion, post_entity $post) : bool {
359         if (!$this->can_view_post_shell($user, $post)) {
360             return false;
361         }
363         $forum = $this->get_forum();
364         $forumrecord = $this->get_forum_record();
365         $discussionrecord = $this->get_discussion_record($discussion);
366         $postrecord = $this->get_post_record($post);
367         $coursemodule = $forum->get_course_module_record();
368         return forum_user_can_see_post($forumrecord, $discussionrecord, $postrecord, $user, $coursemodule, false);
369     }
371     /**
372      * Can the user view the post at all?
373      * In some situations the user can view the shell of a post without being able to view its content.
374      *
375      * @param   stdClass $user The user to check
376      * @param   post_entity $post The post the user wants to view
377      * @return  bool
378      *
379      */
380     public function can_view_post_shell(stdClass $user, post_entity $post) : bool {
381         if (!$post->is_private_reply()) {
382             return true;
383         }
385         if ($post->is_private_reply_intended_for_user($user)) {
386             return true;
387         }
389         return $this->can_view_any_private_reply($user);
390     }
392     /**
393      * Whether the user can view any private reply in the forum.
394      *
395      * @param   stdClass $user The user to check
396      * @return  bool
397      */
398     public function can_view_any_private_reply(stdClass $user) : bool {
399         return has_capability('mod/forum:readprivatereplies', $this->get_context(), $user);
400     }
402     /**
403      * Can the user edit the post in this discussion?
404      *
405      * @param stdClass $user The user to check
406      * @param discussion_entity $discussion The discussion to check
407      * @param post_entity $post The post the user wants to edit
408      * @return bool
409      */
410     public function can_edit_post(stdClass $user, discussion_entity $discussion, post_entity $post) : bool {
411         global $CFG;
413         $context = $this->get_context();
414         $ownpost = $post->is_owned_by_user($user);
415         $ineditingtime = $post->get_age() < $CFG->maxeditingtime;
417         switch ($this->forum->get_type()) {
418             case 'news':
419                 // Allow editing of news posts once the discussion has started.
420                 $ineditingtime = !$post->has_parent() && $discussion->has_started();
421                 break;
422             case 'single':
423                 if ($discussion->is_first_post($post)) {
424                     return has_capability('moodle/course:manageactivities', $context, $user);
425                 }
426                 break;
427         }
429         return ($ownpost && $ineditingtime) || has_capability('mod/forum:editanypost', $context, $user);
430     }
432     /**
433      * Can the user delete the post in this discussion?
434      *
435      * @param stdClass $user The user to check
436      * @param discussion_entity $discussion The discussion to check
437      * @param post_entity $post The post the user wants to delete
438      * @return bool
439      */
440     public function can_delete_post(stdClass $user, discussion_entity $discussion, post_entity $post) : bool {
441         global $CFG;
443         $forum = $this->get_forum();
445         if ($forum->get_type() == 'single' && $discussion->is_first_post($post)) {
446             // Do not allow deleting of first post in single simple type.
447             return false;
448         } else {
449             $context = $this->get_context();
450             $ownpost = $post->is_owned_by_user($user);
451             $ineditingtime = $post->get_age() < $CFG->maxeditingtime;
453             return ($ownpost && $ineditingtime && has_capability('mod/forum:deleteownpost', $context, $user)) ||
454                 has_capability('mod/forum:deleteanypost', $context, $user);
455         }
456     }
458     /**
459      * Can the user split the post in this discussion?
460      *
461      * @param stdClass $user The user to check
462      * @param discussion_entity $discussion The discussion to check
463      * @param post_entity $post The post the user wants to split
464      * @return bool
465      */
466     public function can_split_post(stdClass $user, discussion_entity $discussion, post_entity $post) : bool {
467         if ($post->is_private_reply()) {
468             // It is not possible to create a private discussion.
469             return false;
470         }
472         return $this->can_split_discussions($user) && $post->has_parent();
473     }
475     /**
476      * Can the user reply to the post in this discussion?
477      *
478      * @param stdClass $user The user to check
479      * @param discussion_entity $discussion The discussion to check
480      * @param post_entity $post The post the user wants to reply to
481      * @return bool
482      */
483     public function can_reply_to_post(stdClass $user, discussion_entity $discussion, post_entity $post) : bool {
484         if ($post->is_private_reply()) {
485             // It is not possible to reply to a private reply.
486             return false;
487         }
489         return $this->can_post_in_discussion($user, $discussion);
490     }
492     /**
493      * Can the user reply privately to the specified post?
494      *
495      * @param stdClass $user The user to check
496      * @param post_entity $post The post the user wants to reply to
497      * @return bool
498      */
499     public function can_reply_privately_to_post(stdClass $user, post_entity $post) : bool {
500         if ($post->is_private_reply()) {
501             // You cannot reply privately to a post which is, itself, a private reply.
502             return false;
503         }
505         return has_capability('mod/forum:postprivatereply', $this->get_context(), $user);
506     }
508     /**
509      * Can the user export (see portfolios) the post in this discussion?
510      *
511      * @param stdClass $user The user to check
512      * @param post_entity $post The post the user wants to export
513      * @return bool
514      */
515     public function can_export_post(stdClass $user, post_entity $post) : bool {
516         global $CFG;
517         $context = $this->get_context();
518         return $CFG->enableportfolios  && (has_capability('mod/forum:exportpost', $context, $user) ||
519             ($post->is_owned_by_user($user) && has_capability('mod/forum:exportownpost', $context, $user)));
520     }
522     /**
523      * Get the forum entity for this capability manager.
524      *
525      * @return forum_entity
526      */
527     protected function get_forum() : forum_entity {
528         return $this->forum;
529     }
531     /**
532      * Get the legacy forum record for this forum.
533      *
534      * @return stdClass
535      */
536     protected function get_forum_record() : stdClass {
537         return $this->forumrecord;
538     }
540     /**
541      * Get the context for this capability manager.
542      *
543      * @return context
544      */
545     protected function get_context() : context {
546         return $this->context;
547     }
549     /**
550      * Get the legacy discussion record for the given discussion entity.
551      *
552      * @param discussion_entity $discussion The discussion to convert
553      * @return stdClass
554      */
555     protected function get_discussion_record(discussion_entity $discussion) : stdClass {
556         return $this->discussiondatamapper->to_legacy_object($discussion);
557     }
559     /**
560      * Get the legacy post record for the given post entity.
561      *
562      * @param post_entity $post The post to convert
563      * @return stdClass
564      */
565     protected function get_post_record(post_entity $post) : stdClass {
566         return $this->postdatamapper->to_legacy_object($post);
567     }
569     /**
570      * Can the user view the participants of this discussion?
571      *
572      * @param stdClass $user The user to check
573      * @param discussion_entity $discussion The discussion to check
574      * @return bool
575      */
576     public function can_view_participants(stdClass $user, discussion_entity $discussion) : bool {
577         return course_can_view_participants($this->get_context()) &&
578             !$this->must_post_before_viewing_discussion($user, $discussion);
579     }
581     /**
582      * Can the user view hidden posts in this forum?
583      *
584      * @param stdClass $user The user to check
585      * @return bool
586      */
587     public function can_view_hidden_posts(stdClass $user) : bool {
588         return has_capability('mod/forum:viewhiddentimedposts', $this->get_context(), $user);
589     }
591     /**
592      * Can the user manage this forum?
593      *
594      * @param stdClass $user The user to check
595      * @return bool
596      */
597     public function can_manage_forum(stdClass $user) {
598         return has_capability('moodle/course:manageactivities', $this->get_context(), $user);
599     }
601     /**
602      * Can the user manage tags on the site?
603      *
604      * @param stdClass $user The user to check
605      * @return bool
606      */
607     public function can_manage_tags(stdClass $user) : bool {
608         return has_capability('moodle/tag:manage', context_system::instance(), $user);
609     }
611     /**
612      * Checks whether the user can self enrol into the course.
613      * Mimics the checks on the add button in deprecatedlib/forum_print_latest_discussions
614      *
615      * @param stdClass $user
616      * @return bool
617      */
618     public function can_self_enrol(stdClass $user) : bool {
619         $canstart = false;
621         if ($this->forum->get_type() != 'news') {
622             if (isguestuser($user) or !isloggedin()) {
623                 $canstart = true;
624             }
626             if (!is_enrolled($this->context) and !is_viewing($this->context)) {
627                  // Allow guests and not-logged-in to see the button - they are prompted to log in after clicking the link,
628                  // Normal users with temporary guest access see this button too, they are asked to enrol instead,
629                  // Do not show the button to users with suspended enrolments here.
630                 $canstart = enrol_selfenrol_available($this->forum->get_course_id());
631             }
632         }
634         return $canstart;
635     }
637     /**
638      * Checks whether the user can export the whole forum (discussions and posts).
639      *
640      * @param stdClass $user The user object.
641      * @return bool True if the user can export the forum or false otherwise.
642      */
643     public function can_export_forum(stdClass $user) : bool {
644         return has_capability('mod/forum:exportforum', $this->get_context(), $user);
645     }