Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 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 switch ($this->forum->get_type()) { 123 case 'news': 124 $capability = 'mod/forum:addnews'; 125 break; 126 case 'qanda': 127 $capability = 'mod/forum:addquestion'; 128 break; 129 default: 130 $capability = 'mod/forum:startdiscussion'; 131 } 132 133 if (!has_capability($capability, $this->forum->get_context(), $user)) { 134 return false; 135 } 136 137 if ($this->forum->get_type() == 'eachuser') { 138 if (forum_user_has_posted_discussion($this->forum->get_id(), $user->id, $groupid)) { 139 return false; 140 } 141 } 142 143 if ($this->forum->is_in_group_mode()) { 144 return $groupid ? $this->can_access_group($user, $groupid) : $this->can_access_all_groups($user); 145 } else { 146 return true; 147 } 148 } 149 150 /** 151 * Can the user access all groups? 152 * 153 * @param stdClass $user The user to check 154 * @return bool 155 */ 156 public function can_access_all_groups(stdClass $user) : bool { 157 return has_capability('moodle/site:accessallgroups', $this->get_context(), $user); 158 } 159 160 /** 161 * Can the user access the given group? 162 * 163 * @param stdClass $user The user to check 164 * @param int $groupid The id of the group that the forum is set to 165 * @return bool 166 */ 167 public function can_access_group(stdClass $user, int $groupid) : bool { 168 if ($this->can_access_all_groups($user)) { 169 // This user has access to all groups. 170 return true; 171 } 172 173 // This is a group discussion for a forum in separate groups mode. 174 // Check if the user is a member. 175 // This is the most expensive check. 176 return groups_is_member($groupid, $user->id); 177 } 178 179 /** 180 * Can the user post to their groups? 181 * 182 * @param stdClass $user The user to check 183 * @return bool 184 */ 185 public function can_post_to_my_groups(stdClass $user) : bool { 186 return has_capability('mod/forum:canposttomygroups', $this->get_context(), $user); 187 } 188 189 /** 190 * Can the user view discussions in this forum? 191 * 192 * @param stdClass $user The user to check 193 * @return bool 194 */ 195 public function can_view_discussions(stdClass $user) : bool { 196 return has_capability('mod/forum:viewdiscussion', $this->get_context(), $user); 197 } 198 199 /** 200 * Can the user move discussions in this forum? 201 * 202 * @param stdClass $user The user to check 203 * @return bool 204 */ 205 public function can_move_discussions(stdClass $user) : bool { 206 $forum = $this->get_forum(); 207 return $forum->get_type() !== 'single' && 208 has_capability('mod/forum:movediscussions', $this->get_context(), $user); 209 } 210 211 /** 212 * Can the user pin discussions in this forum? 213 * 214 * @param stdClass $user The user to check 215 * @return bool 216 */ 217 public function can_pin_discussions(stdClass $user) : bool { 218 return $this->forum->get_type() !== 'single' && 219 has_capability('mod/forum:pindiscussions', $this->get_context(), $user); 220 } 221 222 /** 223 * Can the user split discussions in this forum? 224 * 225 * @param stdClass $user The user to check 226 * @return bool 227 */ 228 public function can_split_discussions(stdClass $user) : bool { 229 $forum = $this->get_forum(); 230 return $forum->get_type() !== 'single' && has_capability('mod/forum:splitdiscussions', $this->get_context(), $user); 231 } 232 233 /** 234 * Can the user export (see portfolios) discussions in this forum? 235 * 236 * @param stdClass $user The user to check 237 * @return bool 238 */ 239 public function can_export_discussions(stdClass $user) : bool { 240 global $CFG; 241 return $CFG->enableportfolios && has_capability('mod/forum:exportdiscussion', $this->get_context(), $user); 242 } 243 244 /** 245 * Can the user manually mark posts as read/unread in this forum? 246 * 247 * @param stdClass $user The user to check 248 * @return bool 249 */ 250 public function can_manually_control_post_read_status(stdClass $user) : bool { 251 global $CFG; 252 return $CFG->forum_usermarksread && isloggedin() && forum_tp_is_tracked($this->get_forum_record(), $user); 253 } 254 255 /** 256 * Is the user required to post in the discussion before they can view it? 257 * 258 * @param stdClass $user The user to check 259 * @param discussion_entity $discussion The discussion to check 260 * @return bool 261 */ 262 public function must_post_before_viewing_discussion(stdClass $user, discussion_entity $discussion) : bool { 263 $forum = $this->get_forum(); 264 265 if ($forum->get_type() === 'qanda') { 266 // If it's a Q and A forum then the user must either have the capability to view without 267 // posting or the user must have posted before they can view the discussion. 268 return !has_capability('mod/forum:viewqandawithoutposting', $this->get_context(), $user) && 269 !forum_user_has_posted($forum->get_id(), $discussion->get_id(), $user->id); 270 } else { 271 // No other forum types require posting before viewing. 272 return false; 273 } 274 } 275 276 /** 277 * Can the user subscribe to the give discussion? 278 * 279 * @param stdClass $user The user to check 280 * @param discussion_entity $discussion The discussion to check 281 * @return bool 282 */ 283 public function can_subscribe_to_discussion(stdClass $user, discussion_entity $discussion) : bool { 284 return $this->can_subscribe_to_forum($user); 285 } 286 287 /** 288 * Can the user move the discussion in this forum? 289 * 290 * @param stdClass $user The user to check 291 * @param discussion_entity $discussion The discussion to check 292 * @return bool 293 */ 294 public function can_move_discussion(stdClass $user, discussion_entity $discussion) : bool { 295 return $this->can_move_discussions($user); 296 } 297 298 /** 299 * Is the user pin the discussion? 300 * 301 * @param stdClass $user The user to check 302 * @param discussion_entity $discussion The discussion to check 303 * @return bool 304 */ 305 public function can_pin_discussion(stdClass $user, discussion_entity $discussion) : bool { 306 return $this->can_pin_discussions($user); 307 } 308 309 /** 310 * Can the user post in this discussion? 311 * 312 * @param stdClass $user The user to check 313 * @param discussion_entity $discussion The discussion to check 314 * @return bool 315 */ 316 public function can_post_in_discussion(stdClass $user, discussion_entity $discussion) : bool { 317 $forum = $this->get_forum(); 318 $forumrecord = $this->get_forum_record(); 319 $discussionrecord = $this->get_discussion_record($discussion); 320 $context = $this->get_context(); 321 $coursemodule = $forum->get_course_module_record(); 322 $course = $forum->get_course_record(); 323 324 return forum_user_can_post($forumrecord, $discussionrecord, $user, $coursemodule, $course, $context); 325 } 326 327 /** 328 * Can the user favourite the discussion 329 * 330 * @param stdClass $user The user to check 331 * @return bool 332 */ 333 public function can_favourite_discussion(stdClass $user) : bool { 334 $context = $this->get_context(); 335 return has_capability('mod/forum:cantogglefavourite', $context, $user); 336 } 337 338 /** 339 * Can the user view the content of a discussion? 340 * 341 * @param stdClass $user The user to check 342 * @param discussion_entity $discussion The discussion to check 343 * @return bool 344 */ 345 public function can_view_discussion(stdClass $user, discussion_entity $discussion) : bool { 346 $forumrecord = $this->get_forum_record(); 347 $discussionrecord = $this->get_discussion_record($discussion); 348 $context = $this->get_context(); 349 350 return forum_user_can_see_discussion($forumrecord, $discussionrecord, $context, $user); 351 } 352 353 /** 354 * Can the user view the content of the post in this discussion? 355 * 356 * @param stdClass $user The user to check 357 * @param discussion_entity $discussion The discussion to check 358 * @param post_entity $post The post the user wants to view 359 * @return bool 360 */ 361 public function can_view_post(stdClass $user, discussion_entity $discussion, post_entity $post) : bool { 362 if (!$this->can_view_post_shell($user, $post)) { 363 return false; 364 } 365 366 // Return cached can view if possible. 367 if (isset($this->canviewpostcache[$user->id][$post->get_id()])) { 368 return $this->canviewpostcache[$user->id][$post->get_id()]; 369 } 370 371 // Otherwise, check if the user can see this post. 372 $forum = $this->get_forum(); 373 $forumrecord = $this->get_forum_record(); 374 $discussionrecord = $this->get_discussion_record($discussion); 375 $postrecord = $this->get_post_record($post); 376 $coursemodule = $forum->get_course_module_record(); 377 $canviewpost = forum_user_can_see_post($forumrecord, $discussionrecord, $postrecord, $user, $coursemodule, false); 378 379 // Then cache the result before returning. 380 $this->canviewpostcache[$user->id][$post->get_id()] = $canviewpost; 381 382 return $canviewpost; 383 } 384 385 /** 386 * Can the user view the post at all? 387 * In some situations the user can view the shell of a post without being able to view its content. 388 * 389 * @param stdClass $user The user to check 390 * @param post_entity $post The post the user wants to view 391 * @return bool 392 * 393 */ 394 public function can_view_post_shell(stdClass $user, post_entity $post) : bool { 395 if (!$post->is_private_reply()) { 396 return true; 397 } 398 399 if ($post->is_private_reply_intended_for_user($user)) { 400 return true; 401 } 402 403 return $this->can_view_any_private_reply($user); 404 } 405 406 /** 407 * Whether the user can view any private reply in the forum. 408 * 409 * @param stdClass $user The user to check 410 * @return bool 411 */ 412 public function can_view_any_private_reply(stdClass $user) : bool { 413 return has_capability('mod/forum:readprivatereplies', $this->get_context(), $user); 414 } 415 416 /** 417 * Can the user edit the post in this discussion? 418 * 419 * @param stdClass $user The user to check 420 * @param discussion_entity $discussion The discussion to check 421 * @param post_entity $post The post the user wants to edit 422 * @return bool 423 */ 424 public function can_edit_post(stdClass $user, discussion_entity $discussion, post_entity $post) : bool { 425 global $CFG; 426 427 $context = $this->get_context(); 428 $ownpost = $post->is_owned_by_user($user); 429 $ineditingtime = $post->get_age() < $CFG->maxeditingtime; 430 431 switch ($this->forum->get_type()) { 432 case 'news': 433 // Allow editing of news posts once the discussion has started. 434 $ineditingtime = !$post->has_parent() && $discussion->has_started(); 435 break; 436 case 'single': 437 if ($discussion->is_first_post($post)) { 438 return has_capability('moodle/course:manageactivities', $context, $user); 439 } 440 break; 441 } 442 443 return ($ownpost && $ineditingtime) || has_capability('mod/forum:editanypost', $context, $user); 444 } 445 446 /** 447 * Verifies is the given user can delete a post. 448 * 449 * @param stdClass $user The user to check 450 * @param discussion_entity $discussion The discussion to check 451 * @param post_entity $post The post the user wants to delete 452 * @param bool $hasreplies Whether the post has replies 453 * @return bool 454 * @throws moodle_exception 455 */ 456 public function validate_delete_post(stdClass $user, discussion_entity $discussion, post_entity $post, 457 bool $hasreplies = false) : void { 458 global $CFG; 459 460 $forum = $this->get_forum(); 461 462 if ($forum->get_type() == 'single' && $discussion->is_first_post($post)) { 463 // Do not allow deleting of first post in single simple type. 464 throw new moodle_exception('cannotdeletepost', 'forum'); 465 } 466 467 $context = $this->get_context(); 468 $ownpost = $post->is_owned_by_user($user); 469 $ineditingtime = $post->get_age() < $CFG->maxeditingtime; 470 471 if (!($ownpost && $ineditingtime && has_capability('mod/forum:deleteownpost', $context, $user) || 472 has_capability('mod/forum:deleteanypost', $context, $user))) { 473 474 throw new moodle_exception('cannotdeletepost', 'forum'); 475 } 476 477 if ($post->get_total_score()) { 478 throw new moodle_exception('couldnotdeleteratings', 'rating'); 479 } 480 481 if ($hasreplies && !has_capability('mod/forum:deleteanypost', $context, $user)) { 482 throw new moodle_exception('couldnotdeletereplies', 'forum'); 483 } 484 } 485 486 487 /** 488 * Can the user delete the post in this discussion? 489 * 490 * @param stdClass $user The user to check 491 * @param discussion_entity $discussion The discussion to check 492 * @param post_entity $post The post the user wants to delete 493 * @param bool $hasreplies Whether the post has replies 494 * @return bool 495 */ 496 public function can_delete_post(stdClass $user, discussion_entity $discussion, post_entity $post, 497 bool $hasreplies = false) : bool { 498 499 try { 500 $this->validate_delete_post($user, $discussion, $post, $hasreplies); 501 return true; 502 } catch (moodle_exception $e) { 503 return false; 504 } 505 } 506 507 /** 508 * Can the user split the post in this discussion? 509 * 510 * @param stdClass $user The user to check 511 * @param discussion_entity $discussion The discussion to check 512 * @param post_entity $post The post the user wants to split 513 * @return bool 514 */ 515 public function can_split_post(stdClass $user, discussion_entity $discussion, post_entity $post) : bool { 516 if ($post->is_private_reply()) { 517 // It is not possible to create a private discussion. 518 return false; 519 } 520 521 return $this->can_split_discussions($user) && $post->has_parent(); 522 } 523 524 /** 525 * Can the user reply to 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 reply to 530 * @return bool 531 */ 532 public function can_reply_to_post(stdClass $user, discussion_entity $discussion, post_entity $post) : bool { 533 if ($post->is_private_reply()) { 534 // It is not possible to reply to a private reply. 535 return false; 536 } else if (!$this->can_view_post($user, $discussion, $post)) { 537 // If the user cannot view the post in the first place, the user should not be able to reply to the post. 538 return false; 539 } 540 541 return $this->can_post_in_discussion($user, $discussion); 542 } 543 544 /** 545 * Can the user reply privately to the specified post? 546 * 547 * @param stdClass $user The user to check 548 * @param post_entity $post The post the user wants to reply to 549 * @return bool 550 */ 551 public function can_reply_privately_to_post(stdClass $user, post_entity $post) : bool { 552 if ($post->is_private_reply()) { 553 // You cannot reply privately to a post which is, itself, a private reply. 554 return false; 555 } 556 557 return has_capability('mod/forum:postprivatereply', $this->get_context(), $user); 558 } 559 560 /** 561 * Can the user export (see portfolios) the post in this discussion? 562 * 563 * @param stdClass $user The user to check 564 * @param post_entity $post The post the user wants to export 565 * @return bool 566 */ 567 public function can_export_post(stdClass $user, post_entity $post) : bool { 568 global $CFG; 569 $context = $this->get_context(); 570 return $CFG->enableportfolios && (has_capability('mod/forum:exportpost', $context, $user) || 571 ($post->is_owned_by_user($user) && has_capability('mod/forum:exportownpost', $context, $user))); 572 } 573 574 /** 575 * Get the forum entity for this capability manager. 576 * 577 * @return forum_entity 578 */ 579 protected function get_forum() : forum_entity { 580 return $this->forum; 581 } 582 583 /** 584 * Get the legacy forum record for this forum. 585 * 586 * @return stdClass 587 */ 588 protected function get_forum_record() : stdClass { 589 return $this->forumrecord; 590 } 591 592 /** 593 * Get the context for this capability manager. 594 * 595 * @return context 596 */ 597 protected function get_context() : context { 598 return $this->context; 599 } 600 601 /** 602 * Get the legacy discussion record for the given discussion entity. 603 * 604 * @param discussion_entity $discussion The discussion to convert 605 * @return stdClass 606 */ 607 protected function get_discussion_record(discussion_entity $discussion) : stdClass { 608 return $this->discussiondatamapper->to_legacy_object($discussion); 609 } 610 611 /** 612 * Get the legacy post record for the given post entity. 613 * 614 * @param post_entity $post The post to convert 615 * @return stdClass 616 */ 617 protected function get_post_record(post_entity $post) : stdClass { 618 return $this->postdatamapper->to_legacy_object($post); 619 } 620 621 /** 622 * Can the user view the participants of this discussion? 623 * 624 * @param stdClass $user The user to check 625 * @param discussion_entity $discussion The discussion to check 626 * @return bool 627 */ 628 public function can_view_participants(stdClass $user, discussion_entity $discussion) : bool { 629 return course_can_view_participants($this->get_context()) && 630 !$this->must_post_before_viewing_discussion($user, $discussion); 631 } 632 633 /** 634 * Can the user view hidden posts in this forum? 635 * 636 * @param stdClass $user The user to check 637 * @return bool 638 */ 639 public function can_view_hidden_posts(stdClass $user) : bool { 640 return has_capability('mod/forum:viewhiddentimedposts', $this->get_context(), $user); 641 } 642 643 /** 644 * Can the user manage this forum? 645 * 646 * @param stdClass $user The user to check 647 * @return bool 648 */ 649 public function can_manage_forum(stdClass $user) { 650 return has_capability('moodle/course:manageactivities', $this->get_context(), $user); 651 } 652 653 /** 654 * Can the user manage tags on the site? 655 * 656 * @param stdClass $user The user to check 657 * @return bool 658 */ 659 public function can_manage_tags(stdClass $user) : bool { 660 return has_capability('moodle/tag:manage', context_system::instance(), $user); 661 } 662 663 /** 664 * Checks whether the user can self enrol into the course. 665 * Mimics the checks on the add button in deprecatedlib/forum_print_latest_discussions 666 * 667 * @param stdClass $user 668 * @return bool 669 */ 670 public function can_self_enrol(stdClass $user) : bool { 671 $canstart = false; 672 673 if ($this->forum->get_type() != 'news') { 674 if (isguestuser($user) or !isloggedin()) { 675 $canstart = true; 676 } 677 678 if (!is_enrolled($this->context) and !is_viewing($this->context)) { 679 // Allow guests and not-logged-in to see the button - they are prompted to log in after clicking the link, 680 // Normal users with temporary guest access see this button too, they are asked to enrol instead, 681 // Do not show the button to users with suspended enrolments here. 682 $canstart = enrol_selfenrol_available($this->forum->get_course_id()); 683 } 684 } 685 686 return $canstart; 687 } 688 689 /** 690 * Checks whether the user can export the whole forum (discussions and posts). 691 * 692 * @param stdClass $user The user object. 693 * @return bool True if the user can export the forum or false otherwise. 694 */ 695 public function can_export_forum(stdClass $user) : bool { 696 return has_capability('mod/forum:exportforum', $this->get_context(), $user); 697 } 698 699 /** 700 * Check whether the supplied grader can grade the gradee. 701 * 702 * @param stdClass $grader The user grading 703 * @param stdClass $gradee The user being graded 704 * @return bool 705 */ 706 public function can_grade(stdClass $grader, stdClass $gradee = null): bool { 707 if (!has_capability('mod/forum:grade', $this->get_context(), $grader)) { 708 return false; 709 } 710 711 return true; 712 } 713 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body