Merge branch 'MDL-65017-master' of git://github.com/jleyva/moodle
[moodle.git] / mod / forum / classes / local / managers / capability.php
CommitLineData
47d38303
RW
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
25namespace mod_forum\local\managers;
26
27defined('MOODLE_INTERNAL') || die();
28
29use mod_forum\local\data_mappers\legacy\forum as legacy_forum_data_mapper;
30use mod_forum\local\data_mappers\legacy\discussion as legacy_discussion_data_mapper;
31use mod_forum\local\data_mappers\legacy\post as legacy_post_data_mapper;
32use mod_forum\local\entities\discussion as discussion_entity;
33use mod_forum\local\entities\forum as forum_entity;
34use mod_forum\local\entities\post as post_entity;
35use mod_forum\subscriptions;
36use context;
37use context_system;
38use stdClass;
b12dbcd4 39use moodle_exception;
47d38303
RW
40
41require_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 */
51class 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
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 }
86
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 {
54d38a73 94 if ($this->forum->get_type() == 'single') {
446ba046 95 return false;
54d38a73
MG
96 }
97
47d38303
RW
98 return !is_guest($this->get_context(), $user) &&
99 subscriptions::is_subscribable($this->get_forum_record());
100 }
101
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 }
113
cbf63d8e
SR
114 if ($this->forum->is_cutoff_date_reached()) {
115 if (!has_capability('mod/forum:canoverridecutoff', $this->get_context())) {
116 return false;
117 }
118 }
119
47d38303
RW
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 }
130
131 if (!has_capability($capability, $this->forum->get_context(), $user)) {
132 return false;
133 }
134
54d38a73
MG
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 }
140
47d38303
RW
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 }
147
148 /**
149 * Can the user access all groups?
150 *
151 * @param stdClass $user The user to check
152 * @return bool
153 */
15f7dbe5 154 public function can_access_all_groups(stdClass $user) : bool {
47d38303
RW
155 return has_capability('moodle/site:accessallgroups', $this->get_context(), $user);
156 }
157
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 */
15f7dbe5 165 public function can_access_group(stdClass $user, int $groupid) : bool {
47d38303
RW
166 if ($this->can_access_all_groups($user)) {
167 // This user has access to all groups.
168 return true;
169 }
170
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 }
176
15f7dbe5
RW
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 }
186
47d38303
RW
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 }
196
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();
54d38a73
MG
205 return $forum->get_type() !== 'single' &&
206 has_capability('mod/forum:movediscussions', $this->get_context(), $user);
47d38303
RW
207 }
208
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 {
54d38a73
MG
216 return $this->forum->get_type() !== 'single' &&
217 has_capability('mod/forum:pindiscussions', $this->get_context(), $user);
47d38303
RW
218 }
219
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 }
230
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 }
241
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 }
252
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();
262
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 }
273
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 }
284
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 }
295
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 }
306
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();
321
322 return forum_user_can_post($forumrecord, $discussionrecord, $user, $coursemodule, $course, $context);
323 }
324
99bda8a7
P
325 /**
326 * Can the user favourite the discussion
327 *
328 * @param stdClass $user The user to check
99bda8a7
P
329 * @return bool
330 */
b4a1bbbb 331 public function can_favourite_discussion(stdClass $user) : bool {
99bda8a7 332 $context = $this->get_context();
24962ee1 333 return has_capability('mod/forum:cantogglefavourite', $context, $user);
99bda8a7
P
334 }
335
1a9c60e9
MG
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();
347
348 return forum_user_can_see_discussion($forumrecord, $discussionrecord, $context, $user);
349 }
350
47d38303 351 /**
bc4c7337 352 * Can the user view the content of the post in this discussion?
47d38303
RW
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 {
bc4c7337
AN
360 if (!$this->can_view_post_shell($user, $post)) {
361 return false;
362 }
363
47d38303
RW
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 }
371
bc4c7337
AN
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 {
15dc885a 382 if (!$post->is_private_reply()) {
bc4c7337
AN
383 return true;
384 }
385
386 if ($post->is_private_reply_intended_for_user($user)) {
387 return true;
388 }
389
390 return $this->can_view_any_private_reply($user);
391 }
392
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 }
402
47d38303
RW
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;
413
414 $context = $this->get_context();
415 $ownpost = $post->is_owned_by_user($user);
416 $ineditingtime = $post->get_age() < $CFG->maxeditingtime;
417
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':
50b52ef4
AN
424 if ($discussion->is_first_post($post)) {
425 return has_capability('moodle/course:manageactivities', $context, $user);
426 }
427 break;
47d38303
RW
428 }
429
430 return ($ownpost && $ineditingtime) || has_capability('mod/forum:editanypost', $context, $user);
431 }
432
433 /**
b12dbcd4 434 * Verifies is the given user can delete a post.
47d38303
RW
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
b12dbcd4 439 * @param bool $hasreplies Whether the post has replies
47d38303 440 * @return bool
b12dbcd4 441 * @throws moodle_exception
47d38303 442 */
b12dbcd4
JL
443 public function validate_delete_post(stdClass $user, discussion_entity $discussion, post_entity $post,
444 bool $hasreplies = false) : void {
47d38303
RW
445 global $CFG;
446
447 $forum = $this->get_forum();
448
449 if ($forum->get_type() == 'single' && $discussion->is_first_post($post)) {
450 // Do not allow deleting of first post in single simple type.
b12dbcd4
JL
451 throw new moodle_exception('cannotdeletepost', 'forum');
452 }
453
454 $context = $this->get_context();
455 $ownpost = $post->is_owned_by_user($user);
456 $ineditingtime = $post->get_age() < $CFG->maxeditingtime;
457
458 if (!($ownpost && $ineditingtime && has_capability('mod/forum:deleteownpost', $context, $user) ||
459 has_capability('mod/forum:deleteanypost', $context, $user))) {
460
461 throw new moodle_exception('cannotdeletepost', 'forum');
462 }
463
464 if ($post->get_total_score()) {
465 throw new moodle_exception('couldnotdeleteratings', 'rating');
466 }
467
468 if ($hasreplies && !has_capability('mod/forum:deleteanypost', $context, $user)) {
469 throw new moodle_exception('couldnotdeletereplies', 'forum');
470 }
471 }
472
473
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 {
47d38303 485
b12dbcd4
JL
486 try {
487 $this->validate_delete_post($user, $discussion, $post, $hasreplies);
488 return true;
489 } catch (moodle_exception $e) {
490 return false;
47d38303
RW
491 }
492 }
493
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 {
bc4c7337
AN
503 if ($post->is_private_reply()) {
504 // It is not possible to create a private discussion.
505 return false;
506 }
507
47d38303
RW
508 return $this->can_split_discussions($user) && $post->has_parent();
509 }
510
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 {
bc4c7337
AN
520 if ($post->is_private_reply()) {
521 // It is not possible to reply to a private reply.
522 return false;
523 }
524
47d38303
RW
525 return $this->can_post_in_discussion($user, $discussion);
526 }
527
bc4c7337
AN
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 }
540
541 return has_capability('mod/forum:postprivatereply', $this->get_context(), $user);
542 }
543
47d38303
RW
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 }
557
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 }
566
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 }
575
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 }
584
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 }
594
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 }
604
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 }
616
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 }
626
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 }
636
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 }
92ba55ee
P
646
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;
656
657 if ($this->forum->get_type() != 'news') {
658 if (isguestuser($user) or !isloggedin()) {
659 $canstart = true;
660 }
661
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 }
669
670 return $canstart;
671 }
75af28c7
SL
672
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 }
47d38303 682}