See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 401 and 403]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * Capability manager for the forum. 19 * 20 * @package mod_forum 21 * @copyright 2019 Ryan Wyllie <ryan@moodle.com> 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 25 namespace mod_forum\local\managers; 26 27 defined('MOODLE_INTERNAL') || die(); 28 29 use mod_forum\local\data_mappers\legacy\forum as legacy_forum_data_mapper; 30 use mod_forum\local\data_mappers\legacy\discussion as legacy_discussion_data_mapper; 31 use mod_forum\local\data_mappers\legacy\post as legacy_post_data_mapper; 32 use mod_forum\local\entities\discussion as discussion_entity; 33 use mod_forum\local\entities\forum as forum_entity; 34 use mod_forum\local\entities\post as post_entity; 35 use mod_forum\subscriptions; 36 use context; 37 use context_system; 38 use stdClass; 39 use moodle_exception; 40 41 require_once($CFG->dirroot . '/mod/forum/lib.php'); 42 43 /** 44 * Capability manager for the forum. 45 * 46 * Defines all the business rules for what a user can and can't do in the forum. 47 * 48 * @copyright 2019 Ryan Wyllie <ryan@moodle.com> 49 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 50 */ 51 class capability { 52 /** @var legacy_forum_data_mapper $forumdatamapper Legacy forum data mapper */ 53 private $forumdatamapper; 54 /** @var legacy_discussion_data_mapper $discussiondatamapper Legacy discussion data mapper */ 55 private $discussiondatamapper; 56 /** @var legacy_post_data_mapper $postdatamapper Legacy post data mapper */ 57 private $postdatamapper; 58 /** @var forum_entity $forum Forum entity */ 59 private $forum; 60 /** @var stdClass $forumrecord Legacy forum record */ 61 private $forumrecord; 62 /** @var context $context Module context for the forum */ 63 private $context; 64 /** @var array $canviewpostcache Cache of discussion posts that can be viewed by a user. */ 65 protected $canviewpostcache = []; 66 67 /** 68 * Constructor. 69 * 70 * @param forum_entity $forum The forum entity to manage capabilities for. 71 * @param legacy_forum_data_mapper $forumdatamapper Legacy forum data mapper 72 * @param legacy_discussion_data_mapper $discussiondatamapper Legacy discussion data mapper 73 * @param legacy_post_data_mapper $postdatamapper Legacy post data mapper 74 */ 75 public function __construct( 76 forum_entity $forum, 77 legacy_forum_data_mapper $forumdatamapper, 78 legacy_discussion_data_mapper $discussiondatamapper, 79 legacy_post_data_mapper $postdatamapper 80 ) { 81 $this->forumdatamapper = $forumdatamapper; 82 $this->discussiondatamapper = $discussiondatamapper; 83 $this->postdatamapper = $postdatamapper; 84 $this->forum = $forum; 85 $this->forumrecord = $forumdatamapper->to_legacy_object($forum); 86 $this->context = $forum->get_context(); 87 } 88 89 /** 90 * Can the user subscribe to this forum? 91 * 92 * @param stdClass $user The user to check 93 * @return bool 94 */ 95 public function can_subscribe_to_forum(stdClass $user) : bool { 96 if ($this->forum->get_type() == 'single') { 97 return false; 98 } 99 100 return !is_guest($this->get_context(), $user) && 101 subscriptions::is_subscribable($this->get_forum_record()); 102 } 103 104 /** 105 * Can the user create discussions in this forum? 106 * 107 * @param stdClass $user The user to check 108 * @param int|null $groupid The current activity group id 109 * @return bool 110 */ 111 public function can_create_discussions(stdClass $user, int $groupid = null) : bool { 112 if (isguestuser($user) or !isloggedin()) { 113 return false; 114 } 115 116 if ($this->forum->is_cutoff_date_reached()) { 117 if (!has_capability('mod/forum:canoverridecutoff', $this->get_context())) { 118 return false; 119 } 120 } 121 122 // If the user reaches the number of posts equal to warning/blocking setting then return the value of canpost in $warningobj. 123 $cmrecord = $this->forum->get_course_module_record(); 124 if ($warningobj = forum_check_throttling($this->forumrecord, $cmrecord)) { 125 return $warningobj->canpost; 126 } 127 128 switch ($this->forum->get_type()) { 129 case 'news': 130 $capability = 'mod/forum:addnews'; 131 break; 132 case 'qanda': 133 $capability = 'mod/forum:addquestion'; 134 break; 135 default: 136 $capability = 'mod/forum:startdiscussion'; 137 } 138 139 if (!has_capability($capability, $this->forum->get_context(), $user)) { 140 return false; 141 } 142 143 if ($this->forum->get_type() == 'eachuser') { 144 if (forum_user_has_posted_discussion($this->forum->get_id(), $user->id, $groupid)) { 145 return false; 146 } 147 } 148 149 if ($this->forum->is_in_group_mode()) { 150 return $groupid ? $this->can_access_group($user, $groupid) : $this->can_access_all_groups($user); 151 } else { 152 return true; 153 } 154 } 155 156 /** 157 * Can the user access all groups? 158 * 159 * @param stdClass $user The user to check 160 * @return bool 161 */ 162 public function can_access_all_groups(stdClass $user) : bool { 163 return has_capability('moodle/site:accessallgroups', $this->get_context(), $user); 164 } 165 166 /** 167 * Can the user access the given group? 168 * 169 * @param stdClass $user The user to check 170 * @param int $groupid The id of the group that the forum is set to 171 * @return bool 172 */ 173 public function can_access_group(stdClass $user, int $groupid) : bool { 174 if ($this->can_access_all_groups($user)) { 175 // This user has access to all groups. 176 return true; 177 } 178 179 // This is a group discussion for a forum in separate groups mode. 180 // Check if the user is a member. 181 // This is the most expensive check. 182 return groups_is_member($groupid, $user->id); 183 } 184 185 /** 186 * Can the user post to their groups? 187 * 188 * @param stdClass $user The user to check 189 * @return bool 190 */ 191 public function can_post_to_my_groups(stdClass $user) : bool { 192 return has_capability('mod/forum:canposttomygroups', $this->get_context(), $user); 193 } 194 195 /** 196 * Can the user view discussions in this forum? 197 * 198 * @param stdClass $user The user to check 199 * @return bool 200 */ 201 public function can_view_discussions(stdClass $user) : bool { 202 return has_capability('mod/forum:viewdiscussion', $this->get_context(), $user); 203 } 204 205 /** 206 * Can the user move discussions in this forum? 207 * 208 * @param stdClass $user The user to check 209 * @return bool 210 */ 211 public function can_move_discussions(stdClass $user) : bool { 212 $forum = $this->get_forum(); 213 return $forum->get_type() !== 'single' && 214 has_capability('mod/forum:movediscussions', $this->get_context(), $user); 215 } 216 217 /** 218 * Can the user pin discussions in this forum? 219 * 220 * @param stdClass $user The user to check 221 * @return bool 222 */ 223 public function can_pin_discussions(stdClass $user) : bool { 224 return $this->forum->get_type() !== 'single' && 225 has_capability('mod/forum:pindiscussions', $this->get_context(), $user); 226 } 227 228 /** 229 * Can the user split discussions in this forum? 230 * 231 * @param stdClass $user The user to check 232 * @return bool 233 */ 234 public function can_split_discussions(stdClass $user) : bool { 235 $forum = $this->get_forum(); 236 return $forum->get_type() !== 'single' && has_capability('mod/forum:splitdiscussions', $this->get_context(), $user); 237 } 238 239 /** 240 * Can the user export (see portfolios) discussions in this forum? 241 * 242 * @param stdClass $user The user to check 243 * @return bool 244 */ 245 public function can_export_discussions(stdClass $user) : bool { 246 global $CFG; 247 return $CFG->enableportfolios && has_capability('mod/forum:exportdiscussion', $this->get_context(), $user); 248 } 249 250 /** 251 * Can the user manually mark posts as read/unread in this forum? 252 * 253 * @param stdClass $user The user to check 254 * @return bool 255 */ 256 public function can_manually_control_post_read_status(stdClass $user) : bool { 257 global $CFG; 258 return $CFG->forum_usermarksread && isloggedin() && forum_tp_is_tracked($this->get_forum_record(), $user); 259 } 260 261 /** 262 * Is the user required to post in the discussion before they can view it? 263 * 264 * @param stdClass $user The user to check 265 * @param discussion_entity $discussion The discussion to check 266 * @return bool 267 */ 268 public function must_post_before_viewing_discussion(stdClass $user, discussion_entity $discussion) : bool { 269 $forum = $this->get_forum(); 270 271 if ($forum->get_type() === 'qanda') { 272 // If it's a Q and A forum then the user must either have the capability to view without 273 // posting or the user must have posted before they can view the discussion. 274 return !has_capability('mod/forum:viewqandawithoutposting', $this->get_context(), $user) && 275 !forum_user_has_posted($forum->get_id(), $discussion->get_id(), $user->id); 276 } else { 277 // No other forum types require posting before viewing. 278 return false; 279 } 280 } 281 282 /** 283 * Can the user subscribe to the give discussion? 284 * 285 * @param stdClass $user The user to check 286 * @param discussion_entity $discussion The discussion to check 287 * @return bool 288 */ 289 public function can_subscribe_to_discussion(stdClass $user, discussion_entity $discussion) : bool { 290 return $this->can_subscribe_to_forum($user); 291 } 292 293 /** 294 * Can the user move the discussion in this forum? 295 * 296 * @param stdClass $user The user to check 297 * @param discussion_entity $discussion The discussion to check 298 * @return bool 299 */ 300 public function can_move_discussion(stdClass $user, discussion_entity $discussion) : bool { 301 return $this->can_move_discussions($user); 302 } 303 304 /** 305 * Is the user pin the discussion? 306 * 307 * @param stdClass $user The user to check 308 * @param discussion_entity $discussion The discussion to check 309 * @return bool 310 */ 311 public function can_pin_discussion(stdClass $user, discussion_entity $discussion) : bool { 312 return $this->can_pin_discussions($user); 313 } 314 315 /** 316 * Can the user post in this discussion? 317 * 318 * @param stdClass $user The user to check 319 * @param discussion_entity $discussion The discussion to check 320 * @return bool 321 */ 322 public function can_post_in_discussion(stdClass $user, discussion_entity $discussion) : bool { 323 $forum = $this->get_forum(); 324 $forumrecord = $this->get_forum_record(); 325 326 $discussionrecord = $this->get_discussion_record($discussion); 327 $context = $this->get_context(); 328 $coursemodule = $forum->get_course_module_record(); 329 $course = $forum->get_course_record(); 330 331 $status = forum_user_can_post($forumrecord, $discussionrecord, $user, $coursemodule, $course, $context); 332 333 // If the user reaches the number of posts equal to warning/blocking setting then logically and canpost value with $status. 334 if ($warningobj = forum_check_throttling($forumrecord, $coursemodule)) { 335 return $status && $warningobj->canpost; 336 } 337 return $status; 338 } 339 340 /** 341 * Can the user favourite the discussion 342 * 343 * @param stdClass $user The user to check 344 * @return bool 345 */ 346 public function can_favourite_discussion(stdClass $user) : bool { 347 $context = $this->get_context(); 348 return has_capability('mod/forum:cantogglefavourite', $context, $user); 349 } 350 351 /** 352 * Can the user view the content of a discussion? 353 * 354 * @param stdClass $user The user to check 355 * @param discussion_entity $discussion The discussion to check 356 * @return bool 357 */ 358 public function can_view_discussion(stdClass $user, discussion_entity $discussion) : bool { 359 $forumrecord = $this->get_forum_record(); 360 $discussionrecord = $this->get_discussion_record($discussion); 361 $context = $this->get_context(); 362 363 return forum_user_can_see_discussion($forumrecord, $discussionrecord, $context, $user); 364 } 365 366 /** 367 * Can the user view the content of the post in this discussion? 368 * 369 * @param stdClass $user The user to check 370 * @param discussion_entity $discussion The discussion to check 371 * @param post_entity $post The post the user wants to view 372 * @return bool 373 */ 374 public function can_view_post(stdClass $user, discussion_entity $discussion, post_entity $post) : bool { 375 if (!$this->can_view_post_shell($user, $post)) { 376 return false; 377 } 378 379 // Return cached can view if possible. 380 if (isset($this->canviewpostcache[$user->id][$post->get_id()])) { 381 return $this->canviewpostcache[$user->id][$post->get_id()]; 382 } 383 384 // Otherwise, check if the user can see this post. 385 $forum = $this->get_forum(); 386 $forumrecord = $this->get_forum_record(); 387 $discussionrecord = $this->get_discussion_record($discussion); 388 $postrecord = $this->get_post_record($post); 389 $coursemodule = $forum->get_course_module_record(); 390 $canviewpost = forum_user_can_see_post($forumrecord, $discussionrecord, $postrecord, $user, $coursemodule, false); 391 392 // Then cache the result before returning. 393 $this->canviewpostcache[$user->id][$post->get_id()] = $canviewpost; 394 395 return $canviewpost; 396 } 397 398 /** 399 * Can the user view the post at all? 400 * In some situations the user can view the shell of a post without being able to view its content. 401 * 402 * @param stdClass $user The user to check 403 * @param post_entity $post The post the user wants to view 404 * @return bool 405 * 406 */ 407 public function can_view_post_shell(stdClass $user, post_entity $post) : bool { 408 if ($post->is_owned_by_user($user)) { 409 return true; 410 } 411 412 if (!$post->is_private_reply()) { 413 return true; 414 } 415 416 if ($post->is_private_reply_intended_for_user($user)) { 417 return true; 418 } 419 420 return $this->can_view_any_private_reply($user); 421 } 422 423 /** 424 * Whether the user can view any private reply in the forum. 425 * 426 * @param stdClass $user The user to check 427 * @return bool 428 */ 429 public function can_view_any_private_reply(stdClass $user) : bool { 430 return has_capability('mod/forum:readprivatereplies', $this->get_context(), $user); 431 } 432 433 /** 434 * Can the user edit the post in this discussion? 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 edit 439 * @return bool 440 */ 441 public function can_edit_post(stdClass $user, discussion_entity $discussion, post_entity $post) : bool { 442 global $CFG; 443 444 $context = $this->get_context(); 445 $ownpost = $post->is_owned_by_user($user); 446 $ineditingtime = $post->get_age() < $CFG->maxeditingtime; 447 448 switch ($this->forum->get_type()) { 449 case 'news': 450 // Allow editing of news posts once the discussion has started. 451 $ineditingtime = !$post->has_parent() && $discussion->has_started(); 452 break; 453 case 'single': 454 if ($discussion->is_first_post($post)) { 455 return has_capability('moodle/course:manageactivities', $context, $user); 456 } 457 break; 458 } 459 460 return ($ownpost && $ineditingtime) || has_capability('mod/forum:editanypost', $context, $user); 461 } 462 463 /** 464 * Verifies is the given user can delete a post. 465 * 466 * @param stdClass $user The user to check 467 * @param discussion_entity $discussion The discussion to check 468 * @param post_entity $post The post the user wants to delete 469 * @param bool $hasreplies Whether the post has replies 470 * @return bool 471 * @throws moodle_exception 472 */ 473 public function validate_delete_post(stdClass $user, discussion_entity $discussion, post_entity $post, 474 bool $hasreplies = false) : void { 475 global $CFG; 476 477 $forum = $this->get_forum(); 478 479 if ($forum->get_type() == 'single' && $discussion->is_first_post($post)) { 480 // Do not allow deleting of first post in single simple type. 481 throw new moodle_exception('cannotdeletepost', 'forum'); 482 } 483 484 $context = $this->get_context(); 485 $ownpost = $post->is_owned_by_user($user); 486 $ineditingtime = $post->get_age() < $CFG->maxeditingtime; 487 488 if (!($ownpost && $ineditingtime && has_capability('mod/forum:deleteownpost', $context, $user) || 489 has_capability('mod/forum:deleteanypost', $context, $user))) { 490 491 throw new moodle_exception('cannotdeletepost', 'forum'); 492 } 493 494 if ($post->get_total_score()) { 495 throw new moodle_exception('couldnotdeleteratings', 'rating'); 496 } 497 498 if ($hasreplies && !has_capability('mod/forum:deleteanypost', $context, $user)) { 499 throw new moodle_exception('couldnotdeletereplies', 'forum'); 500 } 501 } 502 503 504 /** 505 * Can the user delete the post in this discussion? 506 * 507 * @param stdClass $user The user to check 508 * @param discussion_entity $discussion The discussion to check 509 * @param post_entity $post The post the user wants to delete 510 * @param bool $hasreplies Whether the post has replies 511 * @return bool 512 */ 513 public function can_delete_post(stdClass $user, discussion_entity $discussion, post_entity $post, 514 bool $hasreplies = false) : bool { 515 516 try { 517 $this->validate_delete_post($user, $discussion, $post, $hasreplies); 518 return true; 519 } catch (moodle_exception $e) { 520 return false; 521 } 522 } 523 524 /** 525 * Can the user split the post in this discussion? 526 * 527 * @param stdClass $user The user to check 528 * @param discussion_entity $discussion The discussion to check 529 * @param post_entity $post The post the user wants to split 530 * @return bool 531 */ 532 public function can_split_post(stdClass $user, discussion_entity $discussion, post_entity $post) : bool { 533 if ($post->is_private_reply()) { 534 // It is not possible to create a private discussion. 535 return false; 536 } 537 538 return $this->can_split_discussions($user) && $post->has_parent(); 539 } 540 541 /** 542 * Can the user reply to the post in this discussion? 543 * 544 * @param stdClass $user The user to check 545 * @param discussion_entity $discussion The discussion to check 546 * @param post_entity $post The post the user wants to reply to 547 * @return bool 548 */ 549 public function can_reply_to_post(stdClass $user, discussion_entity $discussion, post_entity $post) : bool { 550 if ($post->is_private_reply()) { 551 // It is not possible to reply to a private reply. 552 return false; 553 } else if (!$this->can_view_post($user, $discussion, $post)) { 554 // If the user cannot view the post in the first place, the user should not be able to reply to the post. 555 return false; 556 } 557 558 return $this->can_post_in_discussion($user, $discussion); 559 } 560 561 /** 562 * Can the user reply privately to the specified post? 563 * 564 * @param stdClass $user The user to check 565 * @param post_entity $post The post the user wants to reply to 566 * @return bool 567 */ 568 public function can_reply_privately_to_post(stdClass $user, post_entity $post) : bool { 569 if ($post->is_private_reply()) { 570 // You cannot reply privately to a post which is, itself, a private reply. 571 return false; 572 } 573 574 return has_capability('mod/forum:postprivatereply', $this->get_context(), $user); 575 } 576 577 /** 578 * Can the user export (see portfolios) the post in this discussion? 579 * 580 * @param stdClass $user The user to check 581 * @param post_entity $post The post the user wants to export 582 * @return bool 583 */ 584 public function can_export_post(stdClass $user, post_entity $post) : bool { 585 global $CFG; 586 $context = $this->get_context(); 587 return $CFG->enableportfolios && (has_capability('mod/forum:exportpost', $context, $user) || 588 ($post->is_owned_by_user($user) && has_capability('mod/forum:exportownpost', $context, $user))); 589 } 590 591 /** 592 * Get the forum entity for this capability manager. 593 * 594 * @return forum_entity 595 */ 596 protected function get_forum() : forum_entity { 597 return $this->forum; 598 } 599 600 /** 601 * Get the legacy forum record for this forum. 602 * 603 * @return stdClass 604 */ 605 protected function get_forum_record() : stdClass { 606 return $this->forumrecord; 607 } 608 609 /** 610 * Get the context for this capability manager. 611 * 612 * @return context 613 */ 614 protected function get_context() : context { 615 return $this->context; 616 } 617 618 /** 619 * Get the legacy discussion record for the given discussion entity. 620 * 621 * @param discussion_entity $discussion The discussion to convert 622 * @return stdClass 623 */ 624 protected function get_discussion_record(discussion_entity $discussion) : stdClass { 625 return $this->discussiondatamapper->to_legacy_object($discussion); 626 } 627 628 /** 629 * Get the legacy post record for the given post entity. 630 * 631 * @param post_entity $post The post to convert 632 * @return stdClass 633 */ 634 protected function get_post_record(post_entity $post) : stdClass { 635 return $this->postdatamapper->to_legacy_object($post); 636 } 637 638 /** 639 * Can the user view the participants of this discussion? 640 * 641 * @param stdClass $user The user to check 642 * @param discussion_entity $discussion The discussion to check 643 * @return bool 644 */ 645 public function can_view_participants(stdClass $user, discussion_entity $discussion) : bool { 646 return course_can_view_participants($this->get_context()) && 647 !$this->must_post_before_viewing_discussion($user, $discussion); 648 } 649 650 /** 651 * Can the user view hidden posts in this forum? 652 * 653 * @param stdClass $user The user to check 654 * @return bool 655 */ 656 public function can_view_hidden_posts(stdClass $user) : bool { 657 return has_capability('mod/forum:viewhiddentimedposts', $this->get_context(), $user); 658 } 659 660 /** 661 * Can the user manage this forum? 662 * 663 * @param stdClass $user The user to check 664 * @return bool 665 */ 666 public function can_manage_forum(stdClass $user) { 667 return has_capability('moodle/course:manageactivities', $this->get_context(), $user); 668 } 669 670 /** 671 * Can the user manage tags on the site? 672 * 673 * @param stdClass $user The user to check 674 * @return bool 675 */ 676 public function can_manage_tags(stdClass $user) : bool { 677 return has_capability('moodle/tag:manage', context_system::instance(), $user); 678 } 679 680 /** 681 * Checks whether the user can self enrol into the course. 682 * Mimics the checks on the add button in deprecatedlib/forum_print_latest_discussions 683 * 684 * @param stdClass $user 685 * @return bool 686 */ 687 public function can_self_enrol(stdClass $user) : bool { 688 $canstart = false; 689 690 if ($this->forum->get_type() != 'news') { 691 if (isguestuser($user) or !isloggedin()) { 692 $canstart = true; 693 } 694 695 if (!is_enrolled($this->context) and !is_viewing($this->context)) { 696 // Allow guests and not-logged-in to see the button - they are prompted to log in after clicking the link, 697 // Normal users with temporary guest access see this button too, they are asked to enrol instead, 698 // Do not show the button to users with suspended enrolments here. 699 $canstart = enrol_selfenrol_available($this->forum->get_course_id()); 700 } 701 } 702 703 return $canstart; 704 } 705 706 /** 707 * Checks whether the user can export the whole forum (discussions and posts). 708 * 709 * @param stdClass $user The user object. 710 * @return bool True if the user can export the forum or false otherwise. 711 */ 712 public function can_export_forum(stdClass $user) : bool { 713 return has_capability('mod/forum:exportforum', $this->get_context(), $user); 714 } 715 716 /** 717 * Check whether the supplied grader can grade the gradee. 718 * 719 * @param stdClass $grader The user grading 720 * @param stdClass $gradee The user being graded 721 * @return bool 722 */ 723 public function can_grade(stdClass $grader, stdClass $gradee = null): bool { 724 if (!has_capability('mod/forum:grade', $this->get_context(), $grader)) { 725 return false; 726 } 727 728 return true; 729 } 730 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body