Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403] [Versions 402 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 $mailnow = $post->should_mail_now(); 448 449 switch ($this->forum->get_type()) { 450 case 'news': 451 // Allow editing of news posts once the discussion has started. 452 $ineditingtime = !$post->has_parent() && $discussion->has_started(); 453 break; 454 case 'single': 455 if ($discussion->is_first_post($post)) { 456 return has_capability('moodle/course:manageactivities', $context, $user); 457 } 458 break; 459 } 460 461 return ($ownpost && $ineditingtime && !$mailnow) || has_capability('mod/forum:editanypost', $context, $user); 462 } 463 464 /** 465 * Verifies is the given user can delete a post. 466 * 467 * @param stdClass $user The user to check 468 * @param discussion_entity $discussion The discussion to check 469 * @param post_entity $post The post the user wants to delete 470 * @param bool $hasreplies Whether the post has replies 471 * @return bool 472 * @throws moodle_exception 473 */ 474 public function validate_delete_post(stdClass $user, discussion_entity $discussion, post_entity $post, 475 bool $hasreplies = false) : void { 476 global $CFG; 477 478 $forum = $this->get_forum(); 479 480 if ($forum->get_type() == 'single' && $discussion->is_first_post($post)) { 481 // Do not allow deleting of first post in single simple type. 482 throw new moodle_exception('cannotdeletepost', 'forum'); 483 } 484 485 $context = $this->get_context(); 486 $ownpost = $post->is_owned_by_user($user); 487 $ineditingtime = $post->get_age() < $CFG->maxeditingtime; 488 $mailnow = $post->should_mail_now(); 489 490 if (!($ownpost && $ineditingtime && has_capability('mod/forum:deleteownpost', $context, $user) && !$mailnow || 491 has_capability('mod/forum:deleteanypost', $context, $user))) { 492 493 throw new moodle_exception('cannotdeletepost', 'forum'); 494 } 495 496 if ($post->get_total_score()) { 497 throw new moodle_exception('couldnotdeleteratings', 'rating'); 498 } 499 500 if ($hasreplies && !has_capability('mod/forum:deleteanypost', $context, $user)) { 501 throw new moodle_exception('couldnotdeletereplies', 'forum'); 502 } 503 } 504 505 506 /** 507 * Can the user delete the post in this discussion? 508 * 509 * @param stdClass $user The user to check 510 * @param discussion_entity $discussion The discussion to check 511 * @param post_entity $post The post the user wants to delete 512 * @param bool $hasreplies Whether the post has replies 513 * @return bool 514 */ 515 public function can_delete_post(stdClass $user, discussion_entity $discussion, post_entity $post, 516 bool $hasreplies = false) : bool { 517 518 try { 519 $this->validate_delete_post($user, $discussion, $post, $hasreplies); 520 return true; 521 } catch (moodle_exception $e) { 522 return false; 523 } 524 } 525 526 /** 527 * Can the user split the post in this discussion? 528 * 529 * @param stdClass $user The user to check 530 * @param discussion_entity $discussion The discussion to check 531 * @param post_entity $post The post the user wants to split 532 * @return bool 533 */ 534 public function can_split_post(stdClass $user, discussion_entity $discussion, post_entity $post) : bool { 535 if ($post->is_private_reply()) { 536 // It is not possible to create a private discussion. 537 return false; 538 } 539 540 return $this->can_split_discussions($user) && $post->has_parent(); 541 } 542 543 /** 544 * Can the user reply to the post in this discussion? 545 * 546 * @param stdClass $user The user to check 547 * @param discussion_entity $discussion The discussion to check 548 * @param post_entity $post The post the user wants to reply to 549 * @return bool 550 */ 551 public function can_reply_to_post(stdClass $user, discussion_entity $discussion, post_entity $post) : bool { 552 if ($post->is_private_reply()) { 553 // It is not possible to reply to a private reply. 554 return false; 555 } else if (!$this->can_view_post($user, $discussion, $post)) { 556 // If the user cannot view the post in the first place, the user should not be able to reply to the post. 557 return false; 558 } 559 560 return $this->can_post_in_discussion($user, $discussion); 561 } 562 563 /** 564 * Can the user reply privately to the specified post? 565 * 566 * @param stdClass $user The user to check 567 * @param post_entity $post The post the user wants to reply to 568 * @return bool 569 */ 570 public function can_reply_privately_to_post(stdClass $user, post_entity $post) : bool { 571 if ($post->is_private_reply()) { 572 // You cannot reply privately to a post which is, itself, a private reply. 573 return false; 574 } 575 576 return has_capability('mod/forum:postprivatereply', $this->get_context(), $user); 577 } 578 579 /** 580 * Can the user export (see portfolios) the post in this discussion? 581 * 582 * @param stdClass $user The user to check 583 * @param post_entity $post The post the user wants to export 584 * @return bool 585 */ 586 public function can_export_post(stdClass $user, post_entity $post) : bool { 587 global $CFG; 588 $context = $this->get_context(); 589 return $CFG->enableportfolios && (has_capability('mod/forum:exportpost', $context, $user) || 590 ($post->is_owned_by_user($user) && has_capability('mod/forum:exportownpost', $context, $user))); 591 } 592 593 /** 594 * Get the forum entity for this capability manager. 595 * 596 * @return forum_entity 597 */ 598 protected function get_forum() : forum_entity { 599 return $this->forum; 600 } 601 602 /** 603 * Get the legacy forum record for this forum. 604 * 605 * @return stdClass 606 */ 607 protected function get_forum_record() : stdClass { 608 return $this->forumrecord; 609 } 610 611 /** 612 * Get the context for this capability manager. 613 * 614 * @return context 615 */ 616 protected function get_context() : context { 617 return $this->context; 618 } 619 620 /** 621 * Get the legacy discussion record for the given discussion entity. 622 * 623 * @param discussion_entity $discussion The discussion to convert 624 * @return stdClass 625 */ 626 protected function get_discussion_record(discussion_entity $discussion) : stdClass { 627 return $this->discussiondatamapper->to_legacy_object($discussion); 628 } 629 630 /** 631 * Get the legacy post record for the given post entity. 632 * 633 * @param post_entity $post The post to convert 634 * @return stdClass 635 */ 636 protected function get_post_record(post_entity $post) : stdClass { 637 return $this->postdatamapper->to_legacy_object($post); 638 } 639 640 /** 641 * Can the user view the participants of this discussion? 642 * 643 * @param stdClass $user The user to check 644 * @param discussion_entity $discussion The discussion to check 645 * @return bool 646 */ 647 public function can_view_participants(stdClass $user, discussion_entity $discussion) : bool { 648 return course_can_view_participants($this->get_context()) && 649 !$this->must_post_before_viewing_discussion($user, $discussion); 650 } 651 652 /** 653 * Can the user view hidden posts in this forum? 654 * 655 * @param stdClass $user The user to check 656 * @return bool 657 */ 658 public function can_view_hidden_posts(stdClass $user) : bool { 659 return has_capability('mod/forum:viewhiddentimedposts', $this->get_context(), $user); 660 } 661 662 /** 663 * Can the user manage this forum? 664 * 665 * @param stdClass $user The user to check 666 * @return bool 667 */ 668 public function can_manage_forum(stdClass $user) { 669 return has_capability('moodle/course:manageactivities', $this->get_context(), $user); 670 } 671 672 /** 673 * Can the user manage tags on the site? 674 * 675 * @param stdClass $user The user to check 676 * @return bool 677 */ 678 public function can_manage_tags(stdClass $user) : bool { 679 return has_capability('moodle/tag:manage', context_system::instance(), $user); 680 } 681 682 /** 683 * Checks whether the user can self enrol into the course. 684 * Mimics the checks on the add button in deprecatedlib/forum_print_latest_discussions 685 * 686 * @param stdClass $user 687 * @return bool 688 */ 689 public function can_self_enrol(stdClass $user) : bool { 690 $canstart = false; 691 692 if ($this->forum->get_type() != 'news') { 693 if (isguestuser($user) or !isloggedin()) { 694 $canstart = true; 695 } 696 697 if (!is_enrolled($this->context) and !is_viewing($this->context)) { 698 // Allow guests and not-logged-in to see the button - they are prompted to log in after clicking the link, 699 // Normal users with temporary guest access see this button too, they are asked to enrol instead, 700 // Do not show the button to users with suspended enrolments here. 701 $canstart = enrol_selfenrol_available($this->forum->get_course_id()); 702 } 703 } 704 705 return $canstart; 706 } 707 708 /** 709 * Checks whether the user can export the whole forum (discussions and posts). 710 * 711 * @param stdClass $user The user object. 712 * @return bool True if the user can export the forum or false otherwise. 713 */ 714 public function can_export_forum(stdClass $user) : bool { 715 return has_capability('mod/forum:exportforum', $this->get_context(), $user); 716 } 717 718 /** 719 * Check whether the supplied grader can grade the gradee. 720 * 721 * @param stdClass $grader The user grading 722 * @param stdClass $gradee The user being graded 723 * @return bool 724 */ 725 public function can_grade(stdClass $grader, stdClass $gradee = null): bool { 726 if (!has_capability('mod/forum:grade', $this->get_context(), $grader)) { 727 return false; 728 } 729 730 return true; 731 } 732 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body