e653b6f68860d7fa0c7107651e4528bc844aefa2
[moodle.git] / mod / forum / classes / local / exporters / post.php
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/>.
17 /**
18  * Post exporter class.
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  */
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;
34 use context;
35 use core_tag_tag;
36 use renderer_base;
37 use stdClass;
39 require_once($CFG->dirroot . '/mod/forum/lib.php');
41 /**
42  * Post exporter class.
43  *
44  * @copyright  2019 Ryan Wyllie <ryan@moodle.com>
45  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
46  */
47 class post extends exporter {
48     /** @var post_entity $post The post to export */
49     private $post;
51     /**
52      * Constructor.
53      *
54      * @param post_entity $post The post to export
55      * @param array $related List of related data
56      */
57     public function __construct(post_entity $post, array $related = []) {
58         $this->post = $post;
59         return parent::__construct([], $related);
60     }
62     /**
63      * Return the list of additional properties.
64      *
65      * @return array
66      */
67     protected static function define_other_properties() {
68         $attachmentdefinition = stored_file_exporter::read_properties_definition();
69         $attachmentdefinition['urls'] = [
70             'type' => [
71                 'export' => [
72                     'type' => PARAM_URL,
73                     'optional' => true,
74                     'default' => null,
75                     'null' => NULL_ALLOWED
76                 ]
77             ]
78         ];
79         $attachmentdefinition['html'] = [
80             'type' => [
81                 'plagiarism' => [
82                     'type' => PARAM_RAW,
83                     'optional' => true,
84                     'default' => null,
85                     'null' => NULL_ALLOWED
86                 ],
87             ]
88         ];
90         return [
91             'id' => ['type' => PARAM_INT],
92             'subject' => ['type' => PARAM_TEXT],
93             'message' => ['type' => PARAM_RAW],
94             'messageformat' => ['type' => PARAM_INT],
95             'author' => ['type' => author_exporter::read_properties_definition()],
96             'discussionid' => ['type' => PARAM_INT],
97             'hasparent' => ['type' => PARAM_BOOL],
98             'parentid' => [
99                 'type' => PARAM_INT,
100                 'optional' => true,
101                 'default' => null,
102                 'null' => NULL_ALLOWED
103             ],
104             'timecreated' => ['type' => PARAM_INT],
105             'unread' => [
106                 'type' => PARAM_BOOL,
107                 'optional' => true,
108                 'default' => null,
109                 'null' => NULL_ALLOWED
110             ],
111             'isdeleted' => ['type' => PARAM_BOOL],
112             'haswordcount' => ['type' => PARAM_BOOL],
113             'wordcount' => [
114                 'type' => PARAM_INT,
115                 'optional' => true,
116                 'default' => null,
117                 'null' => NULL_ALLOWED
118             ],
119             'capabilities' => [
120                 'type' => [
121                     'view' => ['type' => PARAM_BOOL],
122                     'edit' => ['type' => PARAM_BOOL],
123                     'delete' => ['type' => PARAM_BOOL],
124                     'split' => ['type' => PARAM_BOOL],
125                     'reply' => ['type' => PARAM_BOOL],
126                     'export' => ['type' => PARAM_BOOL],
127                     'controlreadstatus' => ['type' => PARAM_BOOL]
128                 ]
129             ],
130             'urls' => [
131                 'optional' => true,
132                 'default' => null,
133                 'null' => NULL_ALLOWED,
134                 'type' => [
135                     'view' => [
136                         'type' => PARAM_URL,
137                         'optional' => true,
138                         'default' => null,
139                         'null' => NULL_ALLOWED
140                     ],
141                     'viewisolated' => [
142                         'type' => PARAM_URL,
143                         'optional' => true,
144                         'default' => null,
145                         'null' => NULL_ALLOWED
146                     ],
147                     'viewparent' => [
148                         'type' => PARAM_URL,
149                         'optional' => true,
150                         'default' => null,
151                         'null' => NULL_ALLOWED
152                     ],
153                     'edit' => [
154                         'type' => PARAM_URL,
155                         'optional' => true,
156                         'default' => null,
157                         'null' => NULL_ALLOWED
158                     ],
159                     'delete' => [
160                         'type' => PARAM_URL,
161                         'optional' => true,
162                         'default' => null,
163                         'null' => NULL_ALLOWED
164                     ],
165                     'split' => [
166                         'type' => PARAM_URL,
167                         'optional' => true,
168                         'default' => null,
169                         'null' => NULL_ALLOWED
170                     ],
171                     'reply' => [
172                         'type' => PARAM_URL,
173                         'optional' => true,
174                         'default' => null,
175                         'null' => NULL_ALLOWED
176                     ],
177                     'export' => [
178                         'type' => PARAM_URL,
179                         'optional' => true,
180                         'default' => null,
181                         'null' => NULL_ALLOWED
182                     ],
183                     'markasread' => [
184                         'type' => PARAM_URL,
185                         'optional' => true,
186                         'default' => null,
187                         'null' => NULL_ALLOWED
188                     ],
189                     'markasunread' => [
190                         'type' => PARAM_URL,
191                         'optional' => true,
192                         'default' => null,
193                         'null' => NULL_ALLOWED
194                     ],
195                     'discuss' => [
196                         'type' => PARAM_URL,
197                         'optional' => true,
198                         'default' => null,
199                         'null' => NULL_ALLOWED
200                     ]
201                 ]
202             ],
203             'attachments' => [
204                 'multiple' => true,
205                 'type' => $attachmentdefinition
206             ],
207             'tags' => [
208                 'optional' => true,
209                 'default' => null,
210                 'null' => NULL_ALLOWED,
211                 'multiple' => true,
212                 'type' => [
213                     'id' => ['type' => PARAM_INT],
214                     'tagid' => ['type' => PARAM_INT],
215                     'isstandard' => ['type' => PARAM_BOOL],
216                     'displayname' => ['type' => PARAM_TEXT],
217                     'flag' => ['type' => PARAM_BOOL],
218                     'urls' => [
219                         'type' => [
220                             'view' => ['type' => PARAM_URL]
221                         ]
222                     ]
223                 ]
224             ],
225             'html' => [
226                 'optional' => true,
227                 'default' => null,
228                 'null' => NULL_ALLOWED,
229                 'type' => [
230                     'rating' => [
231                         'optional' => true,
232                         'default' => null,
233                         'null' => NULL_ALLOWED,
234                         'type' => PARAM_RAW
235                     ],
236                     'taglist' => [
237                         'optional' => true,
238                         'default' => null,
239                         'null' => NULL_ALLOWED,
240                         'type' => PARAM_RAW
241                     ],
242                     'authorsubheading' => [
243                         'optional' => true,
244                         'default' => null,
245                         'null' => NULL_ALLOWED,
246                         'type' => PARAM_RAW
247                     ],
248                 ]
249             ]
250         ];
251     }
253     /**
254      * Get the additional values to inject while exporting.
255      *
256      * @param renderer_base $output The renderer.
257      * @return array Keys are the property names, values are their values.
258      */
259     protected function get_other_values(renderer_base $output) {
260         global $CFG;
262         $post = $this->post;
263         $authorgroups = $this->related['authorgroups'];
264         $forum = $this->related['forum'];
265         $discussion = $this->related['discussion'];
266         $author = $this->related['author'];
267         $user = $this->related['user'];
268         $context = $this->related['context'];
269         $readreceiptcollection = $this->related['readreceiptcollection'];
270         $rating = $this->related['rating'];
271         $tags = $this->related['tags'];
272         $attachments = $this->related['attachments'];
273         $includehtml = $this->related['includehtml'];
274         $forumrecord = $this->get_forum_record();
275         $discussionrecord = $this->get_discussion_record();
276         $postrecord = $this->get_post_record();
277         $isdeleted = $post->is_deleted();
278         $hasrating = $rating != null;
279         $hastags = !empty($tags);
280         $discussionid = $post->get_discussion_id();
281         $parentid = $post->get_parent_id();
283         $capabilitymanager = $this->related['capabilitymanager'];
284         $canview = $capabilitymanager->can_view_post($user, $discussion, $post);
285         $canedit = $capabilitymanager->can_edit_post($user, $discussion, $post);
286         $candelete = $capabilitymanager->can_delete_post($user, $discussion, $post);
287         $cansplit = $capabilitymanager->can_split_post($user, $discussion, $post);
288         $canreply = $capabilitymanager->can_reply_to_post($user, $discussion, $post);
289         $canexport = $capabilitymanager->can_export_post($user, $post);
290         $cancontrolreadstatus = $capabilitymanager->can_manually_control_post_read_status($user);
292         $urlfactory = $this->related['urlfactory'];
293         $viewurl = $canview ? $urlfactory->get_view_post_url_from_post($post) : null;
294         $viewisolatedurl = $canview ? $urlfactory->get_view_isolated_post_url_from_post($post) : null;
295         $viewparenturl = $post->has_parent() ? $urlfactory->get_view_post_url_from_post_id($discussionid, $parentid) : null;
296         $editurl = $canedit ? $urlfactory->get_edit_post_url_from_post($forum, $post) : null;
297         $deleteurl = $candelete ? $urlfactory->get_delete_post_url_from_post($post) : null;
298         $spliturl = $cansplit ? $urlfactory->get_split_discussion_at_post_url_from_post($post) : null;
299         $replyurl = $canreply ? $urlfactory->get_reply_to_post_url_from_post($post) : null;
300         $exporturl = $canexport ? $urlfactory->get_export_post_url_from_post($post) : null;
301         $markasreadurl = $cancontrolreadstatus ? $urlfactory->get_mark_post_as_read_url_from_post($post) : null;
302         $markasunreadurl = $cancontrolreadstatus ? $urlfactory->get_mark_post_as_unread_url_from_post($post) : null;
303         $discussurl = $canview ? $urlfactory->get_discussion_view_url_from_post($post) : null;
305         $authorexporter = new author_exporter($author, $authorgroups, ($canview && !$isdeleted), $this->related);
306         $exportedauthor = $authorexporter->export($output);
307         // Only bother loading the content if the user can see it.
308         $loadcontent = $canview && !$isdeleted;
309         $exportattachments = $loadcontent && !empty($attachments);
311         if ($loadcontent) {
312             $subject = $post->get_subject();
313             $timecreated = $post->get_time_created();
314             $message = $this->get_message($post);
315         } else {
316             $subject = $isdeleted ? get_string('forumsubjectdeleted', 'forum') : get_string('forumsubjecthidden', 'forum');
317             $message = $isdeleted ? get_string('forumbodydeleted', 'forum') : get_string('forumbodyhidden', 'forum');
318             $timecreated = null;
320             if ($isdeleted) {
321                 $exportedauthor->fullname = null;
322             }
323         }
325         return [
326             'id' => $post->get_id(),
327             'subject' => $subject,
328             'message' => $message,
329             'messageformat' => $post->get_message_format(),
330             'author' => $exportedauthor,
331             'discussionid' => $post->get_discussion_id(),
332             'hasparent' => $post->has_parent(),
333             'parentid' => $post->has_parent() ? $post->get_parent_id() : null,
334             'timecreated' => $timecreated,
335             'unread' => ($loadcontent && $readreceiptcollection) ? !$readreceiptcollection->has_user_read_post($user, $post) : null,
336             'isdeleted' => $isdeleted,
337             'haswordcount' => $forum->should_display_word_count(),
338             'wordcount' => $forum->should_display_word_count() ? count_words($message) : null,
339             'capabilities' => [
340                 'view' => $canview,
341                 'edit' => $canedit,
342                 'delete' => $candelete,
343                 'split' => $cansplit,
344                 'reply' => $canreply,
345                 'export' => $canexport,
346                 'controlreadstatus' => $cancontrolreadstatus
347             ],
348             'urls' => [
349                 'view' => $viewurl ? $viewurl->out(false) : null,
350                 'viewisolated' => $viewisolatedurl ? $viewisolatedurl->out(false) : null,
351                 'viewparent' => $viewparenturl ? $viewparenturl->out(false) : null,
352                 'edit' => $editurl ? $editurl->out(false) : null,
353                 'delete' => $deleteurl ? $deleteurl->out(false) : null,
354                 'split' => $spliturl ? $spliturl->out(false) : null,
355                 'reply' => $replyurl ? $replyurl->out(false) : null,
356                 'export' => $exporturl && $exporturl ? $exporturl->out(false) : null,
357                 'markasread' => $markasreadurl ? $markasreadurl->out(false) : null,
358                 'markasunread' => $markasunreadurl ? $markasunreadurl->out(false) : null,
359                 'discuss' => $discussurl ? $discussurl->out(false) : null,
360             ],
361             'attachments' => ($exportattachments) ? $this->export_attachments($attachments, $post, $output, $canexport) : [],
362             'tags' => ($loadcontent && $hastags) ? $this->export_tags($tags) : [],
363             'html' => $includehtml ? [
364                 'rating' => ($loadcontent && $hasrating) ? $output->render($rating) : null,
365                 'taglist' => ($loadcontent && $hastags) ? $output->tag_list($tags) : null,
366                 'authorsubheading' => ($loadcontent) ? $this->get_author_subheading_html($exportedauthor, $timecreated) : null
367             ] : null
368         ];
369     }
371     /**
372      * Returns a list of objects that are related.
373      *
374      * @return array
375      */
376     protected static function define_related() {
377         return [
378             'legacydatamapperfactory' => 'mod_forum\local\factories\legacy_data_mapper',
379             'capabilitymanager' => 'mod_forum\local\managers\capability',
380             'readreceiptcollection' => 'mod_forum\local\entities\post_read_receipt_collection?',
381             'urlfactory' => 'mod_forum\local\factories\url',
382             'forum' => 'mod_forum\local\entities\forum',
383             'discussion' => 'mod_forum\local\entities\discussion',
384             'author' => 'mod_forum\local\entities\author',
385             'user' => 'stdClass',
386             'context' => 'context',
387             'authorgroups' => 'stdClass[]',
388             'attachments' => '\stored_file[]?',
389             'tags' => '\core_tag_tag[]?',
390             'rating' => 'rating?',
391             'includehtml' => 'bool'
392         ];
393     }
395     /**
396      * Get the message text from a post.
397      *
398      * @param post_entity $post The post
399      * @return string
400      */
401     private function get_message(post_entity $post) : string {
402         $context = $this->related['context'];
403         $message = file_rewrite_pluginfile_urls(
404             $post->get_message(),
405             'pluginfile.php',
406             $context->id,
407             'mod_forum',
408             'post',
409             $post->get_id()
410         );
412         if (!empty($CFG->enableplagiarism)) {
413             require_once($CFG->libdir . '/plagiarismlib.php');
414             $message .= plagiarism_get_links([
415                 'userid' => $post->get_author_id(),
416                 'content' => $message,
417                 'cmid' => $forum->get_course_module_record()->id,
418                 'course' => $forum->get_course_id(),
419                 'forum' => $forum->get_id()
420             ]);
421         }
423         $message = format_text(
424             $message,
425             $post->get_message_format(),
426             (object) [
427                 'para' => false,
428                 'trusted' => $post->is_message_trusted(),
429                 'context' => $context
430             ]
431         );
433         return $message;
434     }
436     /**
437      * Get the exported attachments for a post.
438      *
439      * @param stored_file[] $attachments The list of attachments for the post
440      * @param post_entity $post The post being exported
441      * @param renderer_base $output Renderer base
442      * @param bool $canexport If the user can export the post (relates to portfolios not exporters like this class)
443      * @return array
444      */
445     private function export_attachments(array $attachments, post_entity $post, renderer_base $output, bool $canexport) : array {
446         global $CFG;
448         $urlfactory = $this->related['urlfactory'];
449         $enableplagiarism = $CFG->enableplagiarism;
450         $forum = $this->related['forum'];
451         $context = $this->related['context'];
453         if ($enableplagiarism) {
454             require_once($CFG->libdir . '/plagiarismlib.php' );
455         }
457         return array_map(function($attachment) use (
458             $output,
459             $enableplagiarism,
460             $canexport,
461             $context,
462             $forum,
463             $post,
464             $urlfactory
465         ) {
466             $contextid = $attachment->get_contextid();
467             $exporter = new stored_file_exporter($attachment, ['context' => $context]);
468             $exportedattachment = $exporter->export($output);
469             $exporturl = $canexport ? $urlfactory->get_export_attachment_url_from_post_and_attachment($post, $attachment) : null;
471             if ($enableplagiarism) {
472                 $plagiarismhtml = plagiarism_get_links([
473                     'userid' => $post->get_author_id(),
474                     'file' => $attachment,
475                     'cmid' => $forum->get_course_module_record()->id,
476                     'course' => $forum->get_course_id(),
477                     'forum' => $forum->get_id()
478                 ]);
479             } else {
480                 $plagiarismhtml = null;
481             }
483             $exportedattachment->urls = [
484                 'export' => $exporturl ? $exporturl->out(false) : null
485             ];
486             $exportedattachment->html = [
487                 'plagiarism' => $plagiarismhtml
488             ];
490             return $exportedattachment;
491         }, $attachments);
492     }
494     /**
495      * Export the list of tags.
496      *
497      * @param core_tag_tag[] $tags List of tags to export
498      * @return array
499      */
500     private function export_tags(array $tags) : array {
501         $user = $this->related['user'];
502         $context = $this->related['context'];
503         $capabilitymanager = $this->related['capabilitymanager'];
504         $canmanagetags = $capabilitymanager->can_manage_tags($user);
506         return array_values(array_map(function($tag) use ($context, $canmanagetags) {
507             $viewurl = core_tag_tag::make_url($tag->tagcollid, $tag->rawname, 0, $context->id);
508             return [
509                 'id' => $tag->taginstanceid,
510                 'tagid' => $tag->id,
511                 'isstandard' => $tag->isstandard,
512                 'displayname' => $tag->get_display_name(),
513                 'flag' => $canmanagetags && !empty($tag->flag),
514                 'urls' => [
515                     'view' => $viewurl->out(false)
516                 ]
517             ];
518         }, $tags));
519     }
521     /**
522      * Get the HTML to display as a subheading in a post.
523      *
524      * @param stdClass $exportedauthor The exported author object
525      * @param int $timecreated The post time created timestamp if it's to be displayed
526      */
527     private function get_author_subheading_html(stdClass $exportedauthor, int $timecreated) : string {
528         $fullname = $exportedauthor->fullname;
529         $profileurl = $exportedauthor->urls['profile'] ?? null;
530         $formatteddate = userdate($timecreated, get_string('strftimedaydatetime', 'core_langconfig'));
531         $name = $profileurl ? "<a href=\"{$profileurl}\">{$fullname}</a>" : $fullname;
532         $date = "<time>{$formatteddate}</time>";
533         return get_string('bynameondate', 'mod_forum', ['name' => $name, 'date' => $date]);
534     }
536     /**
537      * Get the legacy forum record.
538      *
539      * @return stdClass
540      */
541     private function get_forum_record() : stdClass {
542         $forumdbdatamapper = $this->related['legacydatamapperfactory']->get_forum_data_mapper();
543         return $forumdbdatamapper->to_legacy_object($this->related['forum']);
544     }
546     /**
547      * Get the legacy discussion record.
548      *
549      * @return stdClass
550      */
551     private function get_discussion_record() : stdClass {
552         $discussiondbdatamapper = $this->related['legacydatamapperfactory']->get_discussion_data_mapper();
553         return $discussiondbdatamapper->to_legacy_object($this->related['discussion']);
554     }
556     /**
557      * Get the legacy post record.
558      *
559      * @return stdClass
560      */
561     private function get_post_record() : stdClass {
562         $postdbdatamapper = $this->related['legacydatamapperfactory']->get_post_data_mapper();
563         return $postdbdatamapper->to_legacy_object($this->post);
564     }