Differences Between: [Versions 310 and 311] [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]
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 $discussionrecord = $this->get_discussion_record($discussion); 326 $context = $this->get_context(); 327 $coursemodule = $forum->get_course_module_record(); 328 $course = $forum->get_course_record(); 329 330 $status = forum_user_can_post($forumrecord, $discussionrecord, $user, $coursemodule, $course, $context); 331 // If the user reaches the number of posts equal to warning/blocking setting then logically and canpost value with $status. 332 if ($warningobj = forum_check_throttling($forumrecord, $coursemodule)) { 333 return $status && $warningobj->canpost; 334 } 335 return $status; 336 } 337 338 /** 339 * Can the user favourite the discussion 340 * 341 * @param stdClass $user The user to check 342 * @return bool 343 */ 344 public function can_favourite_discussion(stdClass $user) : bool { 345 $context = $this->get_context(); 346 return has_capability('mod/forum:cantogglefavourite', $context, $user); 347 } 348 349 /** 350 * Can the user view the content of a discussion? 351 * 352 * @param stdClass $user The user to check 353 * @param discussion_entity $discussion The discussion to check 354 * @return bool 355 */ 356 public function can_view_discussion(stdClass $user, discussion_entity $discussion) : bool { 357 $forumrecord = $this->get_forum_record(); 358 $discussionrecord = $this->get_discussion_record($discussion); 359 $context = $this->get_context(); 360 361 return forum_user_can_see_discussion($forumrecord, $discussionrecord, $context, $user); 362 } 363 364 /** 365 * Can the user view the content of the post in this discussion? 366 * 367 * @param stdClass $user The user to check 368 * @param discussion_entity $discussion The discussion to check 369 * @param post_entity $post The post the user wants to view 370 * @return bool 371 */ 372 public function can_view_post(stdClass $user, discussion_entity $discussion, post_entity $post) : bool { 373 if (!$this->can_view_post_shell($user, $post)) { 374 return false; 375 } 376 377 // Return cached can view if possible. 378 if (isset($this->canviewpostcache[$user->id][$post->get_id()])) { 379 return $this->canviewpostcache[$user->id][$post->get_id()]; 380 } 381 382 // Otherwise, check if the user can see this post. 383 $forum = $this->get_forum(); 384 $forumrecord = $this->get_forum_record(); 385 $discussionrecord = $this->get_discussion_record($discussion); 386 $postrecord = $this->get_post_record($post); 387 $coursemodule = $forum->get_course_module_record(); 388 $canviewpost = forum_user_can_see_post($forumrecord, $discussionrecord, $postrecord, $user, $coursemodule, false); 389 390 // Then cache the result before returning. 391 $this->canviewpostcache[$user->id][$post->get_id()] = $canviewpost; 392 393 return $canviewpost; 394 } 395 396 /** 397 * Can the user view the post at all? 398 * In some situations the user can view the shell of a post without being able to view its content. 399 * 400 * @param stdClass $user The user to check 401 * @param post_entity $post The post the user wants to view 402 * @return bool 403 * 404 */ 405 public function can_view_post_shell(stdClass $user, post_entity $post) : bool { 406 if (!$post->is_private_reply()) { 407 return true; 408 } 409 410 if ($post->is_private_reply_intended_for_user($user)) { 411 return true; 412 } 413 414 return $this->can_view_any_private_reply($user); 415 } 416 417 /** 418 * Whether the user can view any private reply in the forum. 419 * 420 * @param stdClass $user The user to check 421 * @return bool 422 */ 423 public function can_view_any_private_reply(stdClass $user) : bool { 424 return has_capability('mod/forum:readprivatereplies', $this->get_context(), $user); 425 } 426 427 /** 428 * Can the user edit the post in this discussion? 429 * 430 * @param stdClass $user The user to check 431 * @param discussion_entity $discussion The discussion to check 432 * @param post_entity $post The post the user wants to edit 433 * @return bool 434 */ 435 public function can_edit_post(stdClass $user, discussion_entity $discussion, post_entity $post) : bool { 436 global $CFG; 437 438 $context = $this->get_context(); 439 $ownpost = $post->is_owned_by_user($user); 440 $ineditingtime = $post->get_age() < $CFG->maxeditingtime; 441 442 switch ($this->forum->get_type()) { 443 case 'news': 444 // Allow editing of news posts once the discussion has started. 445 $ineditingtime = !$post->has_parent() && $discussion->has_started(); 446 break; 447 case 'single': 448 if ($discussion->is_first_post($post)) { 449 return has_capability('moodle/course:manageactivities', $context, $user); 450 } 451 break; 452 } 453 454 return ($ownpost && $ineditingtime) || has_capability('mod/forum:editanypost', $context, $user); 455 } 456 457 /** 458 * Verifies is the given user can delete a post. 459 * 460 * @param stdClass $user The user to check 461 * @param discussion_entity $discussion The discussion to check 462 * @param post_entity $post The post the user wants to delete 463 * @param bool $hasreplies Whether the post has replies 464 * @return bool 465 * @throws moodle_exception 466 */ 467 public function validate_delete_post(stdClass $user, discussion_entity $discussion, post_entity $post, 468 bool $hasreplies = false) : void { 469 global $CFG; 470 471 $forum = $this->get_forum(); 472 473 if ($forum->get_type() == 'single' && $discussion->is_first_post($post)) { 474 // Do not allow deleting of first post in single simple type. 475 throw new moodle_exception('cannotdeletepost', 'forum'); 476 } 477 478 $context = $this->get_context(); 479 $ownpost = $post->is_owned_by_user($user); 480 $ineditingtime = $post->get_age() < $CFG->maxeditingtime; 481 482 if (!($ownpost && $ineditingtime && has_capability('mod/forum:deleteownpost', $context, $user) || 483 has_capability('mod/forum:deleteanypost', $context, $user))) { 484 485 throw new moodle_exception('cannotdeletepost', 'forum'); 486 } 487 488 if ($post->get_total_score()) { 489 throw new moodle_exception('couldnotdeleteratings', 'rating'); 490 } 491 492 if ($hasreplies && !has_capability('mod/forum:deleteanypost', $context, $user)) { 493 throw new moodle_exception('couldnotdeletereplies', 'forum'); 494 } 495 } 496 497 498 /** 499 * Can the user delete the post in this discussion? 500 * 501 * @param stdClass $user The user to check 502 * @param discussion_entity $discussion The discussion to check 503 * @param post_entity $post The post the user wants to delete 504 * @param bool $hasreplies Whether the post has replies 505 * @return bool 506 */ 507 public function can_delete_post(stdClass $user, discussion_entity $discussion, post_entity $post, 508 bool $hasreplies = false) : bool { 509 510 try { 511 $this->validate_delete_post($user, $discussion, $post, $hasreplies); 512 return true; 513 } catch (moodle_exception $e) { 514 return false; 515 } 516 } 517 518 /** 519 * Can the user split the post in this discussion? 520 * 521 * @param stdClass $user The user to check 522 * @param discussion_entity $discussion The discussion to check 523 * @param post_entity $post The post the user wants to split 524 * @return bool 525 */ 526 public function can_split_post(stdClass $user, discussion_entity $discussion, post_entity $post) : bool { 527 if ($post->is_private_reply()) { 528 // It is not possible to create a private discussion. 529 return false; 530 } 531 532 return $this->can_split_discussions($user) && $post->has_parent(); 533 } 534 535 /** 536 * Can the user reply to the post in this discussion? 537 * 538 * @param stdClass $user The user to check 539 * @param discussion_entity $discussion The discussion to check 540 * @param post_entity $post The post the user wants to reply to 541 * @return bool 542 */ 543 public function can_reply_to_post(stdClass $user, discussion_entity $discussion, post_entity $post) : bool { 544 if ($post->is_private_reply()) { 545 // It is not possible to reply to a private reply. 546 return false; 547 } else if (!$this->can_view_post($user, $discussion, $post)) { 548 // If the user cannot view the post in the first place, the user should not be able to reply to the post. 549 return false; 550 } 551 552 return $this->can_post_in_discussion($user, $discussion); 553 } 554 555 /** 556 * Can the user reply privately to the specified post? 557 * 558 * @param stdClass $user The user to check 559 * @param post_entity $post The post the user wants to reply to 560 * @return bool 561 */ 562 public function can_reply_privately_to_post(stdClass $user, post_entity $post) : bool { 563 if ($post->is_private_reply()) { 564 // You cannot reply privately to a post which is, itself, a private reply. 565 return false; 566 } 567 568 return has_capability('mod/forum:postprivatereply', $this->get_context(), $user); 569 } 570 571 /** 572 * Can the user export (see portfolios) the post in this discussion? 573 * 574 * @param stdClass $user The user to check 575 * @param post_entity $post The post the user wants to export 576 * @return bool 577 */ 578 public function can_export_post(stdClass $user, post_entity $post) : bool { 579 global $CFG; 580 $context = $this->get_context(); 581 return $CFG->enableportfolios && (has_capability('mod/forum:exportpost', $context, $user) || 582 ($post->is_owned_by_user($user) && has_capability('mod/forum:exportownpost', $context, $user))); 583 } 584 585 /** 586 * Get the forum entity for this capability manager. 587 * 588 * @return forum_entity 589 */ 590 protected function get_forum() : forum_entity { 591 return $this->forum; 592 } 593 594 /** 595 * Get the legacy forum record for this forum. 596 * 597 * @return stdClass 598 */ 599 protected function get_forum_record() : stdClass { 600 return $this->forumrecord; 601 } 602 603 /** 604 * Get the context for this capability manager. 605 * 606 * @return context 607 */ 608 protected function get_context() : context { 609 return $this->context; 610 } 611 612 /** 613 * Get the legacy discussion record for the given discussion entity. 614 * 615 * @param discussion_entity $discussion The discussion to convert 616 * @return stdClass 617 */ 618 protected function get_discussion_record(discussion_entity $discussion) : stdClass { 619 return $this->discussiondatamapper->to_legacy_object($discussion); 620 } 621 622 /** 623 * Get the legacy post record for the given post entity. 624 * 625 * @param post_entity $post The post to convert 626 * @return stdClass 627 */ 628 protected function get_post_record(post_entity $post) : stdClass { 629 return $this->postdatamapper->to_legacy_object($post); 630 } 631 632 /** 633 * Can the user view the participants of this discussion? 634 * 635 * @param stdClass $user The user to check 636 * @param discussion_entity $discussion The discussion to check 637 * @return bool 638 */ 639 public function can_view_participants(stdClass $user, discussion_entity $discussion) : bool { 640 return course_can_view_participants($this->get_context()) && 641 !$this->must_post_before_viewing_discussion($user, $discussion); 642 } 643 644 /** 645 * Can the user view hidden posts in this forum? 646 * 647 * @param stdClass $user The user to check 648 * @return bool 649 */ 650 public function can_view_hidden_posts(stdClass $user) : bool { 651 return has_capability('mod/forum:viewhiddentimedposts', $this->get_context(), $user); 652 } 653 654 /** 655 * Can the user manage this forum? 656 * 657 * @param stdClass $user The user to check 658 * @return bool 659 */ 660 public function can_manage_forum(stdClass $user) { 661 return has_capability('moodle/course:manageactivities', $this->get_context(), $user); 662 } 663 664 /** 665 * Can the user manage tags on the site? 666 * 667 * @param stdClass $user The user to check 668 * @return bool 669 */ 670 public function can_manage_tags(stdClass $user) : bool { 671 return has_capability('moodle/tag:manage', context_system::instance(), $user); 672 } 673 674 /** 675 * Checks whether the user can self enrol into the course. 676 * Mimics the checks on the add button in deprecatedlib/forum_print_latest_discussions 677 * 678 * @param stdClass $user 679 * @return bool 680 */ 681 public function can_self_enrol(stdClass $user) : bool { 682 $canstart = false; 683 684 if ($this->forum->get_type() != 'news') { 685 if (isguestuser($user) or !isloggedin()) { 686 $canstart = true; 687 } 688 689 if (!is_enrolled($this->context) and !is_viewing($this->context)) { 690 // Allow guests and not-logged-in to see the button - they are prompted to log in after clicking the link, 691 // Normal users with temporary guest access see this button too, they are asked to enrol instead, 692 // Do not show the button to users with suspended enrolments here. 693 $canstart = enrol_selfenrol_available($this->forum->get_course_id()); 694 } 695 } 696 697 return $canstart; 698 } 699 700 /** 701 * Checks whether the user can export the whole forum (discussions and posts). 702 * 703 * @param stdClass $user The user object. 704 * @return bool True if the user can export the forum or false otherwise. 705 */ 706 public function can_export_forum(stdClass $user) : bool { 707 return has_capability('mod/forum:exportforum', $this->get_context(), $user); 708 } 709 710 /** 711 * Check whether the supplied grader can grade the gradee. 712 * 713 * @param stdClass $grader The user grading 714 * @param stdClass $gradee The user being graded 715 * @return bool 716 */ 717 public function can_grade(stdClass $grader, stdClass $gradee = null): bool { 718 if (!has_capability('mod/forum:grade', $this->get_context(), $grader)) { 719 return false; 720 } 721 722 return true; 723 } 724 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body