See Release Notes
Long Term Support Release
<?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Exported post builder class. * * @package mod_forum * @copyright 2019 Ryan Wyllie <ryan@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\builders; defined('MOODLE_INTERNAL') || die(); use mod_forum\local\entities\discussion as discussion_entity; use mod_forum\local\entities\forum as forum_entity; use mod_forum\local\entities\post as post_entity; use mod_forum\local\factories\legacy_data_mapper as legacy_data_mapper_factory; use mod_forum\local\factories\exporter as exporter_factory; use mod_forum\local\factories\vault as vault_factory;< use context;> use mod_forum\local\factories\manager as manager_factory;use core_tag_tag; use moodle_exception;< use rating_manager;use renderer_base; use stdClass; /** * Exported post builder class. * * This class is an implementation of the builder pattern (loosely). It is responsible * for taking a set of related forums, discussions, and posts and generate the exported * version of the posts. * * It encapsulates the complexity involved with exporting posts. All of the relevant * additional resources will be loaded by this class in order to ensure the exporting * process can happen. * * See this doc for more information on the builder pattern: * https://designpatternsphp.readthedocs.io/en/latest/Creational/Builder/README.html * * @copyright 2019 Ryan Wyllie <ryan@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class exported_posts { /** @var renderer_base $renderer Core renderer */ private $renderer; /** @var legacy_data_mapper_factory $legacydatamapperfactory Data mapper factory */ private $legacydatamapperfactory; /** @var exporter_factory $exporterfactory Exporter factory */ private $exporterfactory; /** @var vault_factory $vaultfactory Vault factory */ private $vaultfactory; /** @var rating_manager $ratingmanager Rating manager */ private $ratingmanager;> /** @var manager_factory $managerfactory Manager factory */ /** > private $managerfactory; * Constructor. >* * @param renderer_base $renderer Core renderer * @param legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory * @param exporter_factory $exporterfactory Exporter factory * @param vault_factory $vaultfactory Vault factory< * @param rating_manager $ratingmanager Rating manager> * @param manager_factory $managerfactory Manager factory*/ public function __construct( renderer_base $renderer, legacy_data_mapper_factory $legacydatamapperfactory, exporter_factory $exporterfactory, vault_factory $vaultfactory,< rating_manager $ratingmanager> manager_factory $managerfactory) { $this->renderer = $renderer; $this->legacydatamapperfactory = $legacydatamapperfactory; $this->exporterfactory = $exporterfactory; $this->vaultfactory = $vaultfactory;< $this->ratingmanager = $ratingmanager;> $this->managerfactory = $managerfactory; > $this->ratingmanager = $managerfactory->get_rating_manager();} /** * Build the exported posts for a given set of forums, discussions, and posts. * * This will typically be used for a list of posts in the same discussion/forum however * it does support exporting any arbitrary list of posts as long as the caller also provides * a unique list of all discussions for the list of posts and all forums for the list of discussions. * * Increasing the number of different forums being processed will increase the processing time * due to processing multiple contexts (for things like capabilities, files, etc). The code attempts * to load the additional resources as efficiently as possible but there is no way around some of * the additional overhead. * * Note: Some posts will be removed as part of the build process according to capabilities. * A one-to-one mapping should not be expected. * * @param stdClass $user The user to export the posts for. * @param forum_entity[] $forums A list of all forums that each of the $discussions belong to * @param discussion_entity[] $discussions A list of all discussions that each of the $posts belong to * @param post_entity[] $posts The list of posts to export.> * @param bool $includeinlineattachments Whether inline attachments should be included or not.* @return stdClass[] List of exported posts in the same order as the $posts array. */ public function build( stdClass $user, array $forums, array $discussions,< array $posts> array $posts, > bool $includeinlineattachments = false) : array { // Format the forums and discussion to make them more easily accessed later. $forums = array_reduce($forums, function($carry, $forum) { $carry[$forum->get_id()] = $forum; return $carry; }, []); $discussions = array_reduce($discussions, function($carry, $discussion) { $carry[$discussion->get_id()] = $discussion; return $carry; }, []); // Group the posts by discussion and forum so that we can load the resources in // batches to improve performance. $groupedposts = $this->group_posts_by_discussion($forums, $discussions, $posts); // Load all of the resources we need in order to export the posts. $authorsbyid = $this->get_authors_for_posts($posts); $authorcontextids = $this->get_author_context_ids(array_keys($authorsbyid)); $attachmentsbypostid = $this->get_attachments_for_posts($groupedposts);> $inlineattachments = []; $groupsbycourseandauthorid = $this->get_author_groups_from_posts($groupedposts); > if ($includeinlineattachments) { $tagsbypostid = $this->get_tags_from_posts($posts); > $inlineattachments = $this->get_inline_attachments_for_posts($groupedposts); $ratingbypostid = $this->get_ratings_from_posts($user, $groupedposts); > }$readreceiptcollectionbyforumid = $this->get_read_receipts_from_posts($user, $groupedposts); $exportedposts = []; // Export each set of posts per discussion because it's the largest chunks we can // break them into due to constraints on capability checks. foreach ($groupedposts as $grouping) { [ 'forum' => $forum, 'discussion' => $discussion, 'posts' => $groupedposts ] = $grouping;> // Exclude posts the user cannot see, such as certain posts in Q and A forums. $forumid = $forum->get_id(); > $capabilitymanager = $this->managerfactory->get_capability_manager($forum); $courseid = $forum->get_course_record()->id; > $groupedposts = array_filter($groupedposts, function($post) use ($capabilitymanager, $user, $discussion) { $postsexporter = $this->exporterfactory->get_posts_exporter( > return $capabilitymanager->can_view_post($user, $discussion, $post); $user, > }); $forum, >$discussion, $groupedposts, $authorsbyid, $authorcontextids, $attachmentsbypostid, $groupsbycourseandauthorid[$courseid], $readreceiptcollectionbyforumid[$forumid] ?? null, $tagsbypostid, $ratingbypostid,< true> true, > $inlineattachments); ['posts' => $exportedgroupedposts] = (array) $postsexporter->export($this->renderer); $exportedposts = array_merge($exportedposts, $exportedgroupedposts); } if (count($forums) == 1 && count($discussions) == 1) { // All of the posts belong to a single discussion in a single forum so // the exported order will match the given $posts array. return $exportedposts; } else { // Since we grouped the posts by discussion and forum the ordering of the // exported posts may be different to the given $posts array so we should // sort it back into the correct order for the caller. return $this->sort_exported_posts($posts, $exportedposts); } } /** * Group the posts by which discussion they belong to in order for them to be processed * in chunks by the exporting. * * Returns a list of groups where each group has a forum, discussion, and list of posts. * E.g. * [ * [ * 'forum' => <forum_entity>, * 'discussion' => <discussion_entity>, * 'posts' => [ * <post_entity in discussion>, * <post_entity in discussion>, * <post_entity in discussion> * ] * ] * ] * * @param forum_entity[] $forums A list of all forums that each of the $discussions belong to, indexed by id. * @param discussion_entity[] $discussions A list of all discussions that each of the $posts belong to, indexed by id. * @param post_entity[] $posts The list of posts to process. * @return array List of grouped posts. Each group has a discussion, forum, and posts. */ private function group_posts_by_discussion(array $forums, array $discussions, array $posts) : array { return array_reduce($posts, function($carry, $post) use ($forums, $discussions) { $discussionid = $post->get_discussion_id(); if (!isset($discussions[$discussionid])) { throw new moodle_exception('Unable to find discussion with id ' . $discussionid); } if (isset($carry[$discussionid])) { $carry[$discussionid]['posts'][] = $post; } else { $discussion = $discussions[$discussionid]; $forumid = $discussion->get_forum_id(); if (!isset($forums[$forumid])) { throw new moodle_exception('Unable to find forum with id ' . $forumid); } $carry[$discussionid] = [ 'forum' => $forums[$forumid], 'discussion' => $discussions[$discussionid], 'posts' => [$post] ]; } return $carry; }, []); } /** * Load the list of authors for the given posts. * * The list of authors will be indexed by the author id. * * @param post_entity[] $posts The list of posts to process. * @return author_entity[] */ private function get_authors_for_posts(array $posts) : array { $authorvault = $this->vaultfactory->get_author_vault(); return $authorvault->get_authors_for_posts($posts); } /** * Get the user context ids for each of the authors. * * @param int[] $authorids The list of author ids to fetch context ids for. * @return int[] Context ids indexed by author id */ private function get_author_context_ids(array $authorids) : array { $authorvault = $this->vaultfactory->get_author_vault(); return $authorvault->get_context_ids_for_author_ids($authorids);> } } > > /** /** > * Load the list of all inline attachments for the posts. The list of attachments will be * Load the list of all attachments for the posts. The list of attachments will be > * indexed by the post id. * indexed by the post id. > * * > * @param array $groupedposts List of posts grouped by discussions. * @param array $groupedposts List of posts grouped by discussions. > * @return stored_file[] * @return stored_file[] > */ */ > private function get_inline_attachments_for_posts(array $groupedposts) : array { private function get_attachments_for_posts(array $groupedposts) : array { > $inlineattachmentsbypostid = []; $attachmentsbypostid = []; > $postattachmentvault = $this->vaultfactory->get_post_attachment_vault(); $postattachmentvault = $this->vaultfactory->get_post_attachment_vault(); > $postsbyforum = array_reduce($groupedposts, function($carry, $grouping) { $postsbyforum = array_reduce($groupedposts, function($carry, $grouping) { > ['forum' => $forum, 'posts' => $posts] = $grouping; ['forum' => $forum, 'posts' => $posts] = $grouping; > > $forumid = $forum->get_id(); $forumid = $forum->get_id(); > if (!isset($carry[$forumid])) { if (!isset($carry[$forumid])) { > $carry[$forumid] = [ $carry[$forumid] = [ > 'forum' => $forum, 'forum' => $forum, > 'posts' => [] 'posts' => [] > ]; ]; > } } > > $carry[$forumid]['posts'] = array_merge($carry[$forumid]['posts'], $posts); $carry[$forumid]['posts'] = array_merge($carry[$forumid]['posts'], $posts); > return $carry; return $carry; > }, []); }, []); > > foreach ($postsbyforum as $grouping) { foreach ($postsbyforum as $grouping) { > ['forum' => $forum, 'posts' => $posts] = $grouping; ['forum' => $forum, 'posts' => $posts] = $grouping; > $inlineattachments = $postattachmentvault->get_inline_attachments_for_posts($forum->get_context(), $posts); $attachments = $postattachmentvault->get_attachments_for_posts($forum->get_context(), $posts); > > // Have to loop in order to maintain the correct indexes since they are numeric. // Have to loop in order to maintain the correct indexes since they are numeric. > foreach ($inlineattachments as $postid => $attachment) { foreach ($attachments as $postid => $attachment) { > $inlineattachmentsbypostid[$postid] = $attachment; $attachmentsbypostid[$postid] = $attachment; > } } > } } > > return $inlineattachmentsbypostid;return $attachmentsbypostid; } /** * Get the groups for each author of the given posts. * * The results are grouped by course and then author id because the groups are * contextually related to the course, e.g. a single author can be part of two different * sets of groups in two different courses. * * @param array $groupedposts List of posts grouped by discussions. * @return array List of groups indexed by forum id and then author id. */ private function get_author_groups_from_posts(array $groupedposts) : array { $groupsbyauthorid = []; $authoridsbycourseid = []; // Get the unique list of author ids for each course in the grouped // posts. Grouping by course is the largest grouping we can achieve. foreach ($groupedposts as $grouping) { ['forum' => $forum, 'posts' => $posts] = $grouping; $course = $forum->get_course_record(); $courseid = $course->id; if (!isset($authoridsbycourseid[$courseid])) { $coursemodule = $forum->get_course_module_record(); $authoridsbycourseid[$courseid] = [ 'groupingid' => $coursemodule->groupingid, 'authorids' => [] ]; } $authorids = array_map(function($post) { return $post->get_author_id(); }, $posts); foreach ($authorids as $authorid) { $authoridsbycourseid[$courseid]['authorids'][$authorid] = $authorid; } } // Load each set of groups per course. foreach ($authoridsbycourseid as $courseid => $values) { ['groupingid' => $groupingid, 'authorids' => $authorids] = $values; $authorgroups = groups_get_all_groups( $courseid, array_keys($authorids), $groupingid, 'g.*, gm.id, gm.groupid, gm.userid' ); if (!isset($groupsbyauthorid[$courseid])) { $groupsbyauthorid[$courseid] = []; } foreach ($authorgroups as $group) { // Clean up data returned from groups_get_all_groups. $userid = $group->userid; $groupid = $group->groupid; unset($group->userid); unset($group->groupid); $group->id = $groupid; if (!isset($groupsbyauthorid[$courseid][$userid])) { $groupsbyauthorid[$courseid][$userid] = []; } $groupsbyauthorid[$courseid][$userid][] = $group; } } return $groupsbyauthorid; } /** * Get the list of tags for each of the posts. The tags will be returned in an * array indexed by the post id. * * @param post_entity[] $posts The list of posts to load tags for. * @return array Sets of tags indexed by post id. */ private function get_tags_from_posts(array $posts) : array { $postids = array_map(function($post) { return $post->get_id(); }, $posts); return core_tag_tag::get_items_tags('mod_forum', 'forum_posts', $postids); } /** * Get the list of ratings for each post. The ratings are returned in an array * indexed by the post id. * * @param stdClass $user The user viewing the ratings. * @param array $groupedposts List of posts grouped by discussions. * @return array Sets of ratings indexed by post id. */ private function get_ratings_from_posts(stdClass $user, array $groupedposts) { $ratingsbypostid = []; $postsdatamapper = $this->legacydatamapperfactory->get_post_data_mapper(); $postsbyforum = array_reduce($groupedposts, function($carry, $grouping) { ['forum' => $forum, 'posts' => $posts] = $grouping; $forumid = $forum->get_id(); if (!isset($carry[$forumid])) { $carry[$forumid] = [ 'forum' => $forum, 'posts' => [] ]; } $carry[$forumid]['posts'] = array_merge($carry[$forumid]['posts'], $posts); return $carry; }, []); foreach ($postsbyforum as $grouping) { ['forum' => $forum, 'posts' => $posts] = $grouping; if (!$forum->has_rating_aggregate()) { continue; } $items = $postsdatamapper->to_legacy_objects($posts); $ratingoptions = (object) [ 'context' => $forum->get_context(), 'component' => 'mod_forum', 'ratingarea' => 'post', 'items' => $items, 'aggregate' => $forum->get_rating_aggregate(), 'scaleid' => $forum->get_scale(), 'userid' => $user->id, 'assesstimestart' => $forum->get_assess_time_start(), 'assesstimefinish' => $forum->get_assess_time_finish() ]; $rm = $this->ratingmanager; $items = $rm->get_ratings($ratingoptions); foreach ($items as $item) { $ratingsbypostid[$item->id] = empty($item->rating) ? null : $item->rating; } } return $ratingsbypostid; } /** * Get the read receipt collections for the given viewing user and each forum. The * receipt collections will only be loaded for posts in forums that the user is tracking. * * The receipt collections are returned in an array indexed by the forum ids. * * @param stdClass $user The user viewing the posts. * @param array $groupedposts List of posts grouped by discussions. */ private function get_read_receipts_from_posts(stdClass $user, array $groupedposts) { $forumdatamapper = $this->legacydatamapperfactory->get_forum_data_mapper(); $trackedforums = []; $trackedpostids = []; foreach ($groupedposts as $group) { ['forum' => $forum, 'posts' => $posts] = $group; $forumid = $forum->get_id(); if (!isset($trackedforums[$forumid])) { $forumrecord = $forumdatamapper->to_legacy_object($forum); $trackedforums[$forumid] = forum_tp_is_tracked($forumrecord, $user); } if ($trackedforums[$forumid]) { foreach ($posts as $post) { $trackedpostids[] = $post->get_id(); } } } if (empty($trackedpostids)) { return []; } // We can just load a single receipt collection for all tracked posts. $receiptvault = $this->vaultfactory->get_post_read_receipt_collection_vault(); $readreceiptcollection = $receiptvault->get_from_user_id_and_post_ids($user->id, $trackedpostids); $receiptsbyforumid = []; // Assign the collection to all forums that are tracked. foreach ($trackedforums as $forumid => $tracked) { if ($tracked) { $receiptsbyforumid[$forumid] = $readreceiptcollection; } } return $receiptsbyforumid; } /** * Sort the list of exported posts back into the same order as the given posts. * The ordering of the exported posts can often deviate from the given posts due * to the process of exporting them so we need to sort them back into the order * that the calling code expected. * * @param post_entity[] $posts The posts in the expected order. * @param stdClass[] $exportedposts The list of exported posts in any order. * @return stdClass[] Sorted exported posts. */ private function sort_exported_posts(array $posts, array $exportedposts) { $postindexes = []; foreach (array_values($posts) as $index => $post) { $postindexes[$post->get_id()] = $index; } $sortedexportedposts = []; foreach ($exportedposts as $exportedpost) { $index = $postindexes[$exportedpost->id]; $sortedexportedposts[$index] = $exportedpost; } return $sortedexportedposts; } }