4528c602d8e25dd0ed9a636616872a88463248cd
[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                     'description' => 'The URL used to export the attachment',
74                     'optional' => true,
75                     'default' => null,
76                     'null' => NULL_ALLOWED
77                 ]
78             ]
79         ];
80         $attachmentdefinition['html'] = [
81             'type' => [
82                 'plagiarism' => [
83                     'type' => PARAM_RAW,
84                     'description' => 'The HTML source for the Plagiarism Response',
85                     'optional' => true,
86                     'default' => null,
87                     'null' => NULL_ALLOWED
88                 ],
89             ]
90         ];
92         return [
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],
100             'parentid' => [
101                 'type' => PARAM_INT,
102                 'optional' => true,
103                 'default' => null,
104                 'null' => NULL_ALLOWED
105             ],
106             'timecreated' => ['type' => PARAM_INT],
107             'unread' => [
108                 'type' => PARAM_BOOL,
109                 'optional' => true,
110                 'default' => null,
111                 'null' => NULL_ALLOWED
112             ],
113             'isdeleted' => ['type' => PARAM_BOOL],
114             'isprivatereply' => ['type' => PARAM_BOOL],
115             'haswordcount' => ['type' => PARAM_BOOL],
116             'wordcount' => [
117                 'type' => PARAM_INT,
118                 'optional' => true,
119                 'default' => null,
120                 'null' => NULL_ALLOWED
121             ],
122             'capabilities' => [
123                 'type' => [
124                     'view' => [
125                         'type' => PARAM_BOOL,
126                         'null' => NULL_ALLOWED,
127                         'description' => 'Whether the user can view the post',
128                     ],
129                     'edit' => [
130                         'type' => PARAM_BOOL,
131                         'null' => NULL_ALLOWED,
132                         'description' => 'Whether the user can edit the post',
133                     ],
134                     'delete' => [
135                         'type' => PARAM_BOOL,
136                         'null' => NULL_ALLOWED,
137                         'description' => 'Whether the user can delete the post',
138                     ],
139                     'split' => [
140                         'type' => PARAM_BOOL,
141                         'null' => NULL_ALLOWED,
142                         'description' => 'Whether the user can split the post',
143                     ],
144                     'reply' => [
145                         'type' => PARAM_BOOL,
146                         'null' => NULL_ALLOWED,
147                         'description' => 'Whether the user can reply to the post',
148                     ],
149                     'export' => [
150                         'type' => PARAM_BOOL,
151                         'null' => NULL_ALLOWED,
152                         'description' => 'Whether the user can export the post',
153                     ],
154                     'controlreadstatus' => [
155                         'type' => PARAM_BOOL,
156                         'null' => NULL_ALLOWED,
157                         'description' => 'Whether the user can control the read status of the post',
158                     ],
159                     'canreplyprivately' => [
160                         'type' => PARAM_BOOL,
161                         'null' => NULL_ALLOWED,
162                         'description' => 'Whether the user can post a private reply',
163                     ]
164                 ]
165             ],
166             'urls' => [
167                 'optional' => true,
168                 'default' => null,
169                 'null' => NULL_ALLOWED,
170                 'type' => [
171                     'view' => [
172                         'description' => 'The URL used to view the post',
173                         'type' => PARAM_URL,
174                         'optional' => true,
175                         'default' => null,
176                         'null' => NULL_ALLOWED
177                     ],
178                     'viewisolated' => [
179                         'description' => 'The URL used to view the post in isolation',
180                         'type' => PARAM_URL,
181                         'optional' => true,
182                         'default' => null,
183                         'null' => NULL_ALLOWED
184                     ],
185                     'viewparent' => [
186                         'description' => 'The URL used to view the parent of the post',
187                         'type' => PARAM_URL,
188                         'optional' => true,
189                         'default' => null,
190                         'null' => NULL_ALLOWED
191                     ],
192                     'edit' => [
193                         'description' => 'The URL used to edit the post',
194                         'type' => PARAM_URL,
195                         'optional' => true,
196                         'default' => null,
197                         'null' => NULL_ALLOWED
198                     ],
199                     'delete' => [
200                         'description' => 'The URL used to delete the post',
201                         'type' => PARAM_URL,
202                         'optional' => true,
203                         'default' => null,
204                         'null' => NULL_ALLOWED
205                     ],
206                     'split' => [
207                         'description' => 'The URL used to split the discussion ' .
208                             'with the selected post being the first post in the new discussion',
209                         'type' => PARAM_URL,
210                         'optional' => true,
211                         'default' => null,
212                         'null' => NULL_ALLOWED
213                     ],
214                     'reply' => [
215                         'description' => 'The URL used to reply to the post',
216                         'type' => PARAM_URL,
217                         'optional' => true,
218                         'default' => null,
219                         'null' => NULL_ALLOWED
220                     ],
221                     'export' => [
222                         'description' => 'The URL used to export the post',
223                         'type' => PARAM_URL,
224                         'optional' => true,
225                         'default' => null,
226                         'null' => NULL_ALLOWED
227                     ],
228                     'markasread' => [
229                         'description' => 'The URL used to mark the post as read',
230                         'type' => PARAM_URL,
231                         'optional' => true,
232                         'default' => null,
233                         'null' => NULL_ALLOWED
234                     ],
235                     'markasunread' => [
236                         'description' => 'The URL used to mark the post as unread',
237                         'type' => PARAM_URL,
238                         'optional' => true,
239                         'default' => null,
240                         'null' => NULL_ALLOWED
241                     ],
242                     'discuss' => [
243                         'type' => PARAM_URL,
244                         'optional' => true,
245                         'default' => null,
246                         'null' => NULL_ALLOWED
247                     ]
248                 ]
249             ],
250             'attachments' => [
251                 'multiple' => true,
252                 'type' => $attachmentdefinition
253             ],
254             'tags' => [
255                 'optional' => true,
256                 'default' => null,
257                 'null' => NULL_ALLOWED,
258                 'multiple' => true,
259                 'type' => [
260                     'id' => [
261                         'type' => PARAM_INT,
262                         'description' => 'The ID of the Tag',
263                         'null' => NULL_NOT_ALLOWED,
264                     ],
265                     'tagid' => [
266                         'type' => PARAM_INT,
267                         'description' => 'The tagid',
268                         'null' => NULL_NOT_ALLOWED,
269                     ],
270                     'isstandard' => [
271                         'type' => PARAM_BOOL,
272                         'description' => 'Whether this is a standard tag',
273                         'null' => NULL_NOT_ALLOWED,
274                     ],
275                     'displayname' => [
276                         'type' => PARAM_TEXT,
277                         'description' => 'The display name of the tag',
278                         'null' => NULL_NOT_ALLOWED,
279                     ],
280                     'flag' => [
281                         'type' => PARAM_BOOL,
282                         'description' => 'Wehther this tag is flagged',
283                         'null' => NULL_NOT_ALLOWED,
284                     ],
285                     'urls' => [
286                         'description' => 'URLs associated with the tag',
287                         'null' => NULL_NOT_ALLOWED,
288                         'type' => [
289                             'view' => [
290                                 'type' => PARAM_URL,
291                                 'description' => 'The URL to view the tag',
292                                 'null' => NULL_NOT_ALLOWED,
293                             ],
294                         ]
295                     ]
296                 ]
297             ],
298             'html' => [
299                 'optional' => true,
300                 'default' => null,
301                 'null' => NULL_ALLOWED,
302                 'type' => [
303                     'rating' => [
304                         'optional' => true,
305                         'default' => null,
306                         'null' => NULL_ALLOWED,
307                         'type' => PARAM_RAW,
308                         'description' => 'The HTML source to rate the post',
309                     ],
310                     'taglist' => [
311                         'optional' => true,
312                         'default' => null,
313                         'null' => NULL_ALLOWED,
314                         'type' => PARAM_RAW,
315                         'description' => 'The HTML source to view the list of tags',
316                     ],
317                     'authorsubheading' => [
318                         'optional' => true,
319                         'default' => null,
320                         'null' => NULL_ALLOWED,
321                         'type' => PARAM_RAW,
322                         'description' => 'The HTML source to view the author details',
323                     ],
324                 ]
325             ]
326         ];
327     }
329     /**
330      * Get the additional values to inject while exporting.
331      *
332      * @param renderer_base $output The renderer.
333      * @return array Keys are the property names, values are their values.
334      */
335     protected function get_other_values(renderer_base $output) {
336         $post = $this->post;
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(
379             $author,
380             $authorcontextid,
381             $authorgroups,
382             ($canview && !$isdeleted),
383             $this->related
384         );
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);
390         if ($loadcontent) {
391             $subject = $post->get_subject();
392             $timecreated = $post->get_time_created();
393             $message = $this->get_message($post);
394         } else {
395             $subject = $isdeleted ? get_string('forumsubjectdeleted', 'forum') : get_string('forumsubjecthidden', 'forum');
396             $message = $isdeleted ? get_string('forumbodydeleted', 'forum') : get_string('forumbodyhidden', 'forum');
397             $timecreated = null;
399             if ($isdeleted) {
400                 $exportedauthor->fullname = null;
401             }
402         }
404         return [
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,
419             'capabilities' => [
420                 'view' => $canview,
421                 'edit' => $canedit,
422                 'delete' => $candelete,
423                 'split' => $cansplit,
424                 'reply' => $canreply,
425                 'export' => $canexport,
426                 'controlreadstatus' => $cancontrolreadstatus,
427                 'canreplyprivately' => $canreplyprivately
428             ],
429             'urls' => [
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,
441             ],
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
448             ] : null
449         ];
450     }
452     /**
453      * Returns a list of objects that are related.
454      *
455      * @return array
456      */
457     protected static function define_related() {
458         return [
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'
473         ];
474     }
476     /**
477      * This method returns the parameters for the post's message to
478      * use with the function external_format_text().
479      *
480      * @return array
481      */
482     protected function get_format_parameters_for_message() {
483         return [
484             'component' => 'mod_forum',
485             'filearea' => 'post',
486             'itemid' => $this->post->get_id(),
487             'options' => [
488                 'para' => false,
489                 'trusted' => $this->post->is_message_trusted()
490             ]
491         ];
492     }
494     /**
495      * Get the message text from a post.
496      *
497      * @param post_entity $post The post
498      * @return string
499      */
500     private function get_message(post_entity $post) : string {
501         global $CFG;
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()
514             ]);
515         }
517         return $message;
518     }
520     /**
521      * Get the exported attachments for a post.
522      *
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)
527      * @return array
528      */
529     private function export_attachments(array $attachments, post_entity $post, renderer_base $output, bool $canexport) : array {
530         global $CFG;
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' );
539         }
541         return array_map(function($attachment) use (
542             $output,
543             $enableplagiarism,
544             $canexport,
545             $context,
546             $forum,
547             $post,
548             $urlfactory
549         ) {
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()
561                 ]);
562             } else {
563                 $plagiarismhtml = null;
564             }
566             $exportedattachment->urls = [
567                 'export' => $exporturl ? $exporturl->out(false) : null
568             ];
569             $exportedattachment->html = [
570                 'plagiarism' => $plagiarismhtml
571             ];
573             return $exportedattachment;
574         }, $attachments);
575     }
577     /**
578      * Export the list of tags.
579      *
580      * @param core_tag_tag[] $tags List of tags to export
581      * @return array
582      */
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);
591             return [
592                 'id' => $tag->taginstanceid,
593                 'tagid' => $tag->id,
594                 'isstandard' => $tag->isstandard,
595                 'displayname' => $tag->get_display_name(),
596                 'flag' => $canmanagetags && !empty($tag->flag),
597                 'urls' => [
598                     'view' => $viewurl->out(false)
599                 ]
600             ];
601         }, $tags));
602     }
604     /**
605      * Get the HTML to display as a subheading in a post.
606      *
607      * @param stdClass $exportedauthor The exported author object
608      * @param int $timecreated The post time created timestamp if it's to be displayed
609      * @return string
610      */
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]);
618     }