2 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
18 * Post exporter class.
21 * @copyright 2019 Ryan Wyllie <ryan@moodle.com>
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 namespace mod_forum\local\exporters;
27 defined('MOODLE_INTERNAL') || die();
29 use mod_forum\local\entities\post as post_entity;
30 use mod_forum\local\exporters\author as author_exporter;
31 use mod_forum\local\factories\exporter as exporter_factory;
32 use core\external\exporter;
33 use core_files\external\stored_file_exporter;
39 require_once($CFG->dirroot . '/mod/forum/lib.php');
42 * Post exporter class.
44 * @copyright 2019 Ryan Wyllie <ryan@moodle.com>
45 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
47 class post extends exporter {
48 /** @var post_entity $post The post to export */
54 * @param post_entity $post The post to export
55 * @param array $related List of related data
57 public function __construct(post_entity $post, array $related = []) {
59 return parent::__construct([], $related);
63 * Return the list of additional properties.
67 protected static function define_other_properties() {
68 $attachmentdefinition = stored_file_exporter::read_properties_definition();
69 $attachmentdefinition['urls'] = [
73 'description' => 'The URL used to export the attachment',
76 'null' => NULL_ALLOWED
80 $attachmentdefinition['html'] = [
84 'description' => 'The HTML source for the Plagiarism Response',
87 'null' => NULL_ALLOWED
93 'id' => ['type' => PARAM_INT],
94 'subject' => ['type' => PARAM_TEXT],
95 'message' => ['type' => PARAM_RAW],
96 'messageformat' => ['type' => PARAM_INT],
97 'author' => ['type' => author_exporter::read_properties_definition()],
98 'discussionid' => ['type' => PARAM_INT],
99 'hasparent' => ['type' => PARAM_BOOL],
104 'null' => NULL_ALLOWED
106 'timecreated' => ['type' => PARAM_INT],
108 'type' => PARAM_BOOL,
111 'null' => NULL_ALLOWED
113 'isdeleted' => ['type' => PARAM_BOOL],
114 'isprivatereply' => ['type' => PARAM_BOOL],
115 'haswordcount' => ['type' => PARAM_BOOL],
120 'null' => NULL_ALLOWED
125 'type' => PARAM_BOOL,
126 'null' => NULL_ALLOWED,
127 'description' => 'Whether the user can view the post',
130 'type' => PARAM_BOOL,
131 'null' => NULL_ALLOWED,
132 'description' => 'Whether the user can edit the post',
135 'type' => PARAM_BOOL,
136 'null' => NULL_ALLOWED,
137 'description' => 'Whether the user can delete the post',
140 'type' => PARAM_BOOL,
141 'null' => NULL_ALLOWED,
142 'description' => 'Whether the user can split the post',
145 'type' => PARAM_BOOL,
146 'null' => NULL_ALLOWED,
147 'description' => 'Whether the user can reply to the post',
150 'type' => PARAM_BOOL,
151 'null' => NULL_ALLOWED,
152 'description' => 'Whether the user can export the post',
154 'controlreadstatus' => [
155 'type' => PARAM_BOOL,
156 'null' => NULL_ALLOWED,
157 'description' => 'Whether the user can control the read status of the post',
159 'canreplyprivately' => [
160 'type' => PARAM_BOOL,
161 'null' => NULL_ALLOWED,
162 'description' => 'Whether the user can post a private reply',
169 'null' => NULL_ALLOWED,
172 'description' => 'The URL used to view the post',
176 'null' => NULL_ALLOWED
179 'description' => 'The URL used to view the post in isolation',
183 'null' => NULL_ALLOWED
186 'description' => 'The URL used to view the parent of the post',
190 'null' => NULL_ALLOWED
193 'description' => 'The URL used to edit the post',
197 'null' => NULL_ALLOWED
200 'description' => 'The URL used to delete the post',
204 'null' => NULL_ALLOWED
207 'description' => 'The URL used to split the discussion ' .
208 'with the selected post being the first post in the new discussion',
212 'null' => NULL_ALLOWED
215 'description' => 'The URL used to reply to the post',
219 'null' => NULL_ALLOWED
222 'description' => 'The URL used to export the post',
226 'null' => NULL_ALLOWED
229 'description' => 'The URL used to mark the post as read',
233 'null' => NULL_ALLOWED
236 'description' => 'The URL used to mark the post as unread',
240 'null' => NULL_ALLOWED
246 'null' => NULL_ALLOWED
252 'type' => $attachmentdefinition
257 'null' => NULL_ALLOWED,
262 'description' => 'The ID of the Tag',
263 'null' => NULL_NOT_ALLOWED,
267 'description' => 'The tagid',
268 'null' => NULL_NOT_ALLOWED,
271 'type' => PARAM_BOOL,
272 'description' => 'Whether this is a standard tag',
273 'null' => NULL_NOT_ALLOWED,
276 'type' => PARAM_TEXT,
277 'description' => 'The display name of the tag',
278 'null' => NULL_NOT_ALLOWED,
281 'type' => PARAM_BOOL,
282 'description' => 'Wehther this tag is flagged',
283 'null' => NULL_NOT_ALLOWED,
286 'description' => 'URLs associated with the tag',
287 'null' => NULL_NOT_ALLOWED,
291 'description' => 'The URL to view the tag',
292 'null' => NULL_NOT_ALLOWED,
301 'null' => NULL_ALLOWED,
306 'null' => NULL_ALLOWED,
308 'description' => 'The HTML source to rate the post',
313 'null' => NULL_ALLOWED,
315 'description' => 'The HTML source to view the list of tags',
317 'authorsubheading' => [
320 'null' => NULL_ALLOWED,
322 'description' => 'The HTML source to view the author details',
330 * Get the additional values to inject while exporting.
332 * @param renderer_base $output The renderer.
333 * @return array Keys are the property names, values are their values.
335 protected function get_other_values(renderer_base $output) {
337 $authorgroups = $this->related['authorgroups'];
338 $forum = $this->related['forum'];
339 $discussion = $this->related['discussion'];
340 $author = $this->related['author'];
341 $authorcontextid = $this->related['authorcontextid'];
342 $user = $this->related['user'];
343 $readreceiptcollection = $this->related['readreceiptcollection'];
344 $rating = $this->related['rating'];
345 $tags = $this->related['tags'];
346 $attachments = $this->related['attachments'];
347 $includehtml = $this->related['includehtml'];
348 $isdeleted = $post->is_deleted();
349 $isprivatereply = $post->is_private_reply();
350 $hasrating = $rating != null;
351 $hastags = !empty($tags);
352 $discussionid = $post->get_discussion_id();
353 $parentid = $post->get_parent_id();
355 $capabilitymanager = $this->related['capabilitymanager'];
356 $canview = $capabilitymanager->can_view_post($user, $discussion, $post);
357 $canedit = $capabilitymanager->can_edit_post($user, $discussion, $post);
358 $candelete = $capabilitymanager->can_delete_post($user, $discussion, $post);
359 $cansplit = $capabilitymanager->can_split_post($user, $discussion, $post);
360 $canreply = $capabilitymanager->can_reply_to_post($user, $discussion, $post);
361 $canexport = $capabilitymanager->can_export_post($user, $post);
362 $cancontrolreadstatus = $capabilitymanager->can_manually_control_post_read_status($user);
363 $canreplyprivately = $capabilitymanager->can_reply_privately_to_post($user, $post);
365 $urlfactory = $this->related['urlfactory'];
366 $viewurl = $canview ? $urlfactory->get_view_post_url_from_post($post) : null;
367 $viewisolatedurl = $canview ? $urlfactory->get_view_isolated_post_url_from_post($post) : null;
368 $viewparenturl = $post->has_parent() ? $urlfactory->get_view_post_url_from_post_id($discussionid, $parentid) : null;
369 $editurl = $canedit ? $urlfactory->get_edit_post_url_from_post($forum, $post) : null;
370 $deleteurl = $candelete ? $urlfactory->get_delete_post_url_from_post($post) : null;
371 $spliturl = $cansplit ? $urlfactory->get_split_discussion_at_post_url_from_post($post) : null;
372 $replyurl = $canreply ? $urlfactory->get_reply_to_post_url_from_post($post) : null;
373 $exporturl = $canexport ? $urlfactory->get_export_post_url_from_post($post) : null;
374 $markasreadurl = $cancontrolreadstatus ? $urlfactory->get_mark_post_as_read_url_from_post($post) : null;
375 $markasunreadurl = $cancontrolreadstatus ? $urlfactory->get_mark_post_as_unread_url_from_post($post) : null;
376 $discussurl = $canview ? $urlfactory->get_discussion_view_url_from_post($post) : null;
378 $authorexporter = new author_exporter(
382 ($canview && !$isdeleted),
385 $exportedauthor = $authorexporter->export($output);
386 // Only bother loading the content if the user can see it.
387 $loadcontent = $canview && !$isdeleted;
388 $exportattachments = $loadcontent && !empty($attachments);
391 $subject = $post->get_subject();
392 $timecreated = $post->get_time_created();
393 $message = $this->get_message($post);
395 $subject = $isdeleted ? get_string('forumsubjectdeleted', 'forum') : get_string('forumsubjecthidden', 'forum');
396 $message = $isdeleted ? get_string('forumbodydeleted', 'forum') : get_string('forumbodyhidden', 'forum');
400 $exportedauthor->fullname = null;
405 'id' => $post->get_id(),
406 'subject' => $subject,
407 'message' => $message,
408 'messageformat' => $post->get_message_format(),
409 'author' => $exportedauthor,
410 'discussionid' => $post->get_discussion_id(),
411 'hasparent' => $post->has_parent(),
412 'parentid' => $post->has_parent() ? $post->get_parent_id() : null,
413 'timecreated' => $timecreated,
414 'unread' => ($loadcontent && $readreceiptcollection) ? !$readreceiptcollection->has_user_read_post($user, $post) : null,
415 'isdeleted' => $isdeleted,
416 'isprivatereply' => $isprivatereply,
417 'haswordcount' => $forum->should_display_word_count(),
418 'wordcount' => $forum->should_display_word_count() ? count_words($message) : null,
422 'delete' => $candelete,
423 'split' => $cansplit,
424 'reply' => $canreply,
425 'export' => $canexport,
426 'controlreadstatus' => $cancontrolreadstatus,
427 'canreplyprivately' => $canreplyprivately
430 'view' => $viewurl ? $viewurl->out(false) : null,
431 'viewisolated' => $viewisolatedurl ? $viewisolatedurl->out(false) : null,
432 'viewparent' => $viewparenturl ? $viewparenturl->out(false) : null,
433 'edit' => $editurl ? $editurl->out(false) : null,
434 'delete' => $deleteurl ? $deleteurl->out(false) : null,
435 'split' => $spliturl ? $spliturl->out(false) : null,
436 'reply' => $replyurl ? $replyurl->out(false) : null,
437 'export' => $exporturl && $exporturl ? $exporturl->out(false) : null,
438 'markasread' => $markasreadurl ? $markasreadurl->out(false) : null,
439 'markasunread' => $markasunreadurl ? $markasunreadurl->out(false) : null,
440 'discuss' => $discussurl ? $discussurl->out(false) : null,
442 'attachments' => ($exportattachments) ? $this->export_attachments($attachments, $post, $output, $canexport) : [],
443 'tags' => ($loadcontent && $hastags) ? $this->export_tags($tags) : [],
444 'html' => $includehtml ? [
445 'rating' => ($loadcontent && $hasrating) ? $output->render($rating) : null,
446 'taglist' => ($loadcontent && $hastags) ? $output->tag_list($tags) : null,
447 'authorsubheading' => ($loadcontent) ? $this->get_author_subheading_html($exportedauthor, $timecreated) : null
453 * Returns a list of objects that are related.
457 protected static function define_related() {
459 'capabilitymanager' => 'mod_forum\local\managers\capability',
460 'readreceiptcollection' => 'mod_forum\local\entities\post_read_receipt_collection?',
461 'urlfactory' => 'mod_forum\local\factories\url',
462 'forum' => 'mod_forum\local\entities\forum',
463 'discussion' => 'mod_forum\local\entities\discussion',
464 'author' => 'mod_forum\local\entities\author',
465 'authorcontextid' => 'int?',
466 'user' => 'stdClass',
467 'context' => 'context',
468 'authorgroups' => 'stdClass[]',
469 'attachments' => '\stored_file[]?',
470 'tags' => '\core_tag_tag[]?',
471 'rating' => 'rating?',
472 'includehtml' => 'bool'
477 * This method returns the parameters for the post's message to
478 * use with the function external_format_text().
482 protected function get_format_parameters_for_message() {
484 'component' => 'mod_forum',
485 'filearea' => 'post',
486 'itemid' => $this->post->get_id(),
489 'trusted' => $this->post->is_message_trusted()
495 * Get the message text from a post.
497 * @param post_entity $post The post
500 private function get_message(post_entity $post) : string {
503 $message = $post->get_message();
505 if (!empty($CFG->enableplagiarism)) {
506 require_once($CFG->libdir . '/plagiarismlib.php');
507 $forum = $this->related['forum'];
508 $message .= plagiarism_get_links([
509 'userid' => $post->get_author_id(),
510 'content' => $message,
511 'cmid' => $forum->get_course_module_record()->id,
512 'course' => $forum->get_course_id(),
513 'forum' => $forum->get_id()
521 * Get the exported attachments for a post.
523 * @param stored_file[] $attachments The list of attachments for the post
524 * @param post_entity $post The post being exported
525 * @param renderer_base $output Renderer base
526 * @param bool $canexport If the user can export the post (relates to portfolios not exporters like this class)
529 private function export_attachments(array $attachments, post_entity $post, renderer_base $output, bool $canexport) : array {
532 $urlfactory = $this->related['urlfactory'];
533 $enableplagiarism = $CFG->enableplagiarism;
534 $forum = $this->related['forum'];
535 $context = $this->related['context'];
537 if ($enableplagiarism) {
538 require_once($CFG->libdir . '/plagiarismlib.php' );
541 return array_map(function($attachment) use (
550 $exporter = new stored_file_exporter($attachment, ['context' => $context]);
551 $exportedattachment = $exporter->export($output);
552 $exporturl = $canexport ? $urlfactory->get_export_attachment_url_from_post_and_attachment($post, $attachment) : null;
554 if ($enableplagiarism) {
555 $plagiarismhtml = plagiarism_get_links([
556 'userid' => $post->get_author_id(),
557 'file' => $attachment,
558 'cmid' => $forum->get_course_module_record()->id,
559 'course' => $forum->get_course_id(),
560 'forum' => $forum->get_id()
563 $plagiarismhtml = null;
566 $exportedattachment->urls = [
567 'export' => $exporturl ? $exporturl->out(false) : null
569 $exportedattachment->html = [
570 'plagiarism' => $plagiarismhtml
573 return $exportedattachment;
578 * Export the list of tags.
580 * @param core_tag_tag[] $tags List of tags to export
583 private function export_tags(array $tags) : array {
584 $user = $this->related['user'];
585 $context = $this->related['context'];
586 $capabilitymanager = $this->related['capabilitymanager'];
587 $canmanagetags = $capabilitymanager->can_manage_tags($user);
589 return array_values(array_map(function($tag) use ($context, $canmanagetags) {
590 $viewurl = core_tag_tag::make_url($tag->tagcollid, $tag->rawname, 0, $context->id);
592 'id' => $tag->taginstanceid,
594 'isstandard' => $tag->isstandard,
595 'displayname' => $tag->get_display_name(),
596 'flag' => $canmanagetags && !empty($tag->flag),
598 'view' => $viewurl->out(false)
605 * Get the HTML to display as a subheading in a post.
607 * @param stdClass $exportedauthor The exported author object
608 * @param int $timecreated The post time created timestamp if it's to be displayed
611 private function get_author_subheading_html(stdClass $exportedauthor, int $timecreated) : string {
612 $fullname = $exportedauthor->fullname;
613 $profileurl = $exportedauthor->urls['profile'] ?? null;
614 $formatteddate = userdate($timecreated, get_string('strftimedaydatetime', 'core_langconfig'));
615 $name = $profileurl ? "<a href=\"{$profileurl}\">{$fullname}</a>" : $fullname;
616 $date = "<time>{$formatteddate}</time>";
617 return get_string('bynameondate', 'mod_forum', ['name' => $name, 'date' => $date]);