MDL-65675 forum: Remove duplicate Re in subjects
[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             'replysubject' => ['type' => PARAM_TEXT],
96             'message' => ['type' => PARAM_RAW],
97             'messageformat' => ['type' => PARAM_INT],
98             'author' => ['type' => author_exporter::read_properties_definition()],
99             'discussionid' => ['type' => PARAM_INT],
100             'hasparent' => ['type' => PARAM_BOOL],
101             'parentid' => [
102                 'type' => PARAM_INT,
103                 'optional' => true,
104                 'default' => null,
105                 'null' => NULL_ALLOWED
106             ],
107             'timecreated' => ['type' => PARAM_INT],
108             'unread' => [
109                 'type' => PARAM_BOOL,
110                 'optional' => true,
111                 'default' => null,
112                 'null' => NULL_ALLOWED
113             ],
114             'isdeleted' => ['type' => PARAM_BOOL],
115             'isprivatereply' => ['type' => PARAM_BOOL],
116             'haswordcount' => ['type' => PARAM_BOOL],
117             'wordcount' => [
118                 'type' => PARAM_INT,
119                 'optional' => true,
120                 'default' => null,
121                 'null' => NULL_ALLOWED
122             ],
123             'capabilities' => [
124                 'type' => [
125                     'view' => [
126                         'type' => PARAM_BOOL,
127                         'null' => NULL_ALLOWED,
128                         'description' => 'Whether the user can view the post',
129                     ],
130                     'edit' => [
131                         'type' => PARAM_BOOL,
132                         'null' => NULL_ALLOWED,
133                         'description' => 'Whether the user can edit the post',
134                     ],
135                     'delete' => [
136                         'type' => PARAM_BOOL,
137                         'null' => NULL_ALLOWED,
138                         'description' => 'Whether the user can delete the post',
139                     ],
140                     'split' => [
141                         'type' => PARAM_BOOL,
142                         'null' => NULL_ALLOWED,
143                         'description' => 'Whether the user can split the post',
144                     ],
145                     'reply' => [
146                         'type' => PARAM_BOOL,
147                         'null' => NULL_ALLOWED,
148                         'description' => 'Whether the user can reply to the post',
149                     ],
150                     'export' => [
151                         'type' => PARAM_BOOL,
152                         'null' => NULL_ALLOWED,
153                         'description' => 'Whether the user can export the post',
154                     ],
155                     'controlreadstatus' => [
156                         'type' => PARAM_BOOL,
157                         'null' => NULL_ALLOWED,
158                         'description' => 'Whether the user can control the read status of the post',
159                     ],
160                     'canreplyprivately' => [
161                         'type' => PARAM_BOOL,
162                         'null' => NULL_ALLOWED,
163                         'description' => 'Whether the user can post a private reply',
164                     ]
165                 ]
166             ],
167             'urls' => [
168                 'optional' => true,
169                 'default' => null,
170                 'null' => NULL_ALLOWED,
171                 'type' => [
172                     'view' => [
173                         'description' => 'The URL used to view the post',
174                         'type' => PARAM_URL,
175                         'optional' => true,
176                         'default' => null,
177                         'null' => NULL_ALLOWED
178                     ],
179                     'viewisolated' => [
180                         'description' => 'The URL used to view the post in isolation',
181                         'type' => PARAM_URL,
182                         'optional' => true,
183                         'default' => null,
184                         'null' => NULL_ALLOWED
185                     ],
186                     'viewparent' => [
187                         'description' => 'The URL used to view the parent of the post',
188                         'type' => PARAM_URL,
189                         'optional' => true,
190                         'default' => null,
191                         'null' => NULL_ALLOWED
192                     ],
193                     'edit' => [
194                         'description' => 'The URL used to edit the post',
195                         'type' => PARAM_URL,
196                         'optional' => true,
197                         'default' => null,
198                         'null' => NULL_ALLOWED
199                     ],
200                     'delete' => [
201                         'description' => 'The URL used to delete the post',
202                         'type' => PARAM_URL,
203                         'optional' => true,
204                         'default' => null,
205                         'null' => NULL_ALLOWED
206                     ],
207                     'split' => [
208                         'description' => 'The URL used to split the discussion ' .
209                             'with the selected post being the first post in the new discussion',
210                         'type' => PARAM_URL,
211                         'optional' => true,
212                         'default' => null,
213                         'null' => NULL_ALLOWED
214                     ],
215                     'reply' => [
216                         'description' => 'The URL used to reply to the post',
217                         'type' => PARAM_URL,
218                         'optional' => true,
219                         'default' => null,
220                         'null' => NULL_ALLOWED
221                     ],
222                     'export' => [
223                         'description' => 'The URL used to export the post',
224                         'type' => PARAM_URL,
225                         'optional' => true,
226                         'default' => null,
227                         'null' => NULL_ALLOWED
228                     ],
229                     'markasread' => [
230                         'description' => 'The URL used to mark the post as read',
231                         'type' => PARAM_URL,
232                         'optional' => true,
233                         'default' => null,
234                         'null' => NULL_ALLOWED
235                     ],
236                     'markasunread' => [
237                         'description' => 'The URL used to mark the post as unread',
238                         'type' => PARAM_URL,
239                         'optional' => true,
240                         'default' => null,
241                         'null' => NULL_ALLOWED
242                     ],
243                     'discuss' => [
244                         'type' => PARAM_URL,
245                         'optional' => true,
246                         'default' => null,
247                         'null' => NULL_ALLOWED
248                     ]
249                 ]
250             ],
251             'attachments' => [
252                 'multiple' => true,
253                 'type' => $attachmentdefinition
254             ],
255             'tags' => [
256                 'optional' => true,
257                 'default' => null,
258                 'null' => NULL_ALLOWED,
259                 'multiple' => true,
260                 'type' => [
261                     'id' => [
262                         'type' => PARAM_INT,
263                         'description' => 'The ID of the Tag',
264                         'null' => NULL_NOT_ALLOWED,
265                     ],
266                     'tagid' => [
267                         'type' => PARAM_INT,
268                         'description' => 'The tagid',
269                         'null' => NULL_NOT_ALLOWED,
270                     ],
271                     'isstandard' => [
272                         'type' => PARAM_BOOL,
273                         'description' => 'Whether this is a standard tag',
274                         'null' => NULL_NOT_ALLOWED,
275                     ],
276                     'displayname' => [
277                         'type' => PARAM_TEXT,
278                         'description' => 'The display name of the tag',
279                         'null' => NULL_NOT_ALLOWED,
280                     ],
281                     'flag' => [
282                         'type' => PARAM_BOOL,
283                         'description' => 'Wehther this tag is flagged',
284                         'null' => NULL_NOT_ALLOWED,
285                     ],
286                     'urls' => [
287                         'description' => 'URLs associated with the tag',
288                         'null' => NULL_NOT_ALLOWED,
289                         'type' => [
290                             'view' => [
291                                 'type' => PARAM_URL,
292                                 'description' => 'The URL to view the tag',
293                                 'null' => NULL_NOT_ALLOWED,
294                             ],
295                         ]
296                     ]
297                 ]
298             ],
299             'html' => [
300                 'optional' => true,
301                 'default' => null,
302                 'null' => NULL_ALLOWED,
303                 'type' => [
304                     'rating' => [
305                         'optional' => true,
306                         'default' => null,
307                         'null' => NULL_ALLOWED,
308                         'type' => PARAM_RAW,
309                         'description' => 'The HTML source to rate the post',
310                     ],
311                     'taglist' => [
312                         'optional' => true,
313                         'default' => null,
314                         'null' => NULL_ALLOWED,
315                         'type' => PARAM_RAW,
316                         'description' => 'The HTML source to view the list of tags',
317                     ],
318                     'authorsubheading' => [
319                         'optional' => true,
320                         'default' => null,
321                         'null' => NULL_ALLOWED,
322                         'type' => PARAM_RAW,
323                         'description' => 'The HTML source to view the author details',
324                     ],
325                 ]
326             ]
327         ];
328     }
330     /**
331      * Get the additional values to inject while exporting.
332      *
333      * @param renderer_base $output The renderer.
334      * @return array Keys are the property names, values are their values.
335      */
336     protected function get_other_values(renderer_base $output) {
337         $post = $this->post;
338         $authorgroups = $this->related['authorgroups'];
339         $forum = $this->related['forum'];
340         $discussion = $this->related['discussion'];
341         $author = $this->related['author'];
342         $authorcontextid = $this->related['authorcontextid'];
343         $user = $this->related['user'];
344         $readreceiptcollection = $this->related['readreceiptcollection'];
345         $rating = $this->related['rating'];
346         $tags = $this->related['tags'];
347         $attachments = $this->related['attachments'];
348         $includehtml = $this->related['includehtml'];
349         $isdeleted = $post->is_deleted();
350         $isprivatereply = $post->is_private_reply();
351         $hasrating = $rating != null;
352         $hastags = !empty($tags);
353         $discussionid = $post->get_discussion_id();
354         $parentid = $post->get_parent_id();
356         $capabilitymanager = $this->related['capabilitymanager'];
357         $canview = $capabilitymanager->can_view_post($user, $discussion, $post);
358         $canedit = $capabilitymanager->can_edit_post($user, $discussion, $post);
359         $candelete = $capabilitymanager->can_delete_post($user, $discussion, $post);
360         $cansplit = $capabilitymanager->can_split_post($user, $discussion, $post);
361         $canreply = $capabilitymanager->can_reply_to_post($user, $discussion, $post);
362         $canexport = $capabilitymanager->can_export_post($user, $post);
363         $cancontrolreadstatus = $capabilitymanager->can_manually_control_post_read_status($user);
364         $canreplyprivately = $capabilitymanager->can_reply_privately_to_post($user, $post);
366         $urlfactory = $this->related['urlfactory'];
367         $viewurl = $canview ? $urlfactory->get_view_post_url_from_post($post) : null;
368         $viewisolatedurl = $canview ? $urlfactory->get_view_isolated_post_url_from_post($post) : null;
369         $viewparenturl = $post->has_parent() ? $urlfactory->get_view_post_url_from_post_id($discussionid, $parentid) : null;
370         $editurl = $canedit ? $urlfactory->get_edit_post_url_from_post($forum, $post) : null;
371         $deleteurl = $candelete ? $urlfactory->get_delete_post_url_from_post($post) : null;
372         $spliturl = $cansplit ? $urlfactory->get_split_discussion_at_post_url_from_post($post) : null;
373         $replyurl = $canreply ? $urlfactory->get_reply_to_post_url_from_post($post) : null;
374         $exporturl = $canexport ? $urlfactory->get_export_post_url_from_post($post) : null;
375         $markasreadurl = $cancontrolreadstatus ? $urlfactory->get_mark_post_as_read_url_from_post($post) : null;
376         $markasunreadurl = $cancontrolreadstatus ? $urlfactory->get_mark_post_as_unread_url_from_post($post) : null;
377         $discussurl = $canview ? $urlfactory->get_discussion_view_url_from_post($post) : null;
379         $authorexporter = new author_exporter(
380             $author,
381             $authorcontextid,
382             $authorgroups,
383             ($canview && !$isdeleted),
384             $this->related
385         );
386         $exportedauthor = $authorexporter->export($output);
387         // Only bother loading the content if the user can see it.
388         $loadcontent = $canview && !$isdeleted;
389         $exportattachments = $loadcontent && !empty($attachments);
391         if ($loadcontent) {
392             $subject = $post->get_subject();
393             $timecreated = $post->get_time_created();
394             $message = $this->get_message($post);
395         } else {
396             $subject = $isdeleted ? get_string('forumsubjectdeleted', 'forum') : get_string('forumsubjecthidden', 'forum');
397             $message = $isdeleted ? get_string('forumbodydeleted', 'forum') : get_string('forumbodyhidden', 'forum');
398             $timecreated = null;
400             if ($isdeleted) {
401                 $exportedauthor->fullname = null;
402             }
403         }
405         $replysubject = $subject;
406         $strre = get_string('re', 'forum');
407         if (!(substr($replysubject, 0, strlen($strre)) == $strre)) {
408             $replysubject = "{$strre} {$replysubject}";
409         }
411         return [
412             'id' => $post->get_id(),
413             'subject' => $subject,
414             'replysubject' => $replysubject,
415             'message' => $message,
416             'messageformat' => $post->get_message_format(),
417             'author' => $exportedauthor,
418             'discussionid' => $post->get_discussion_id(),
419             'hasparent' => $post->has_parent(),
420             'parentid' => $post->has_parent() ? $post->get_parent_id() : null,
421             'timecreated' => $timecreated,
422             'unread' => ($loadcontent && $readreceiptcollection) ? !$readreceiptcollection->has_user_read_post($user, $post) : null,
423             'isdeleted' => $isdeleted,
424             'isprivatereply' => $isprivatereply,
425             'haswordcount' => $forum->should_display_word_count(),
426             'wordcount' => $forum->should_display_word_count() ? count_words($message) : null,
427             'capabilities' => [
428                 'view' => $canview,
429                 'edit' => $canedit,
430                 'delete' => $candelete,
431                 'split' => $cansplit,
432                 'reply' => $canreply,
433                 'export' => $canexport,
434                 'controlreadstatus' => $cancontrolreadstatus,
435                 'canreplyprivately' => $canreplyprivately
436             ],
437             'urls' => [
438                 'view' => $viewurl ? $viewurl->out(false) : null,
439                 'viewisolated' => $viewisolatedurl ? $viewisolatedurl->out(false) : null,
440                 'viewparent' => $viewparenturl ? $viewparenturl->out(false) : null,
441                 'edit' => $editurl ? $editurl->out(false) : null,
442                 'delete' => $deleteurl ? $deleteurl->out(false) : null,
443                 'split' => $spliturl ? $spliturl->out(false) : null,
444                 'reply' => $replyurl ? $replyurl->out(false) : null,
445                 'export' => $exporturl && $exporturl ? $exporturl->out(false) : null,
446                 'markasread' => $markasreadurl ? $markasreadurl->out(false) : null,
447                 'markasunread' => $markasunreadurl ? $markasunreadurl->out(false) : null,
448                 'discuss' => $discussurl ? $discussurl->out(false) : null,
449             ],
450             'attachments' => ($exportattachments) ? $this->export_attachments($attachments, $post, $output, $canexport) : [],
451             'tags' => ($loadcontent && $hastags) ? $this->export_tags($tags) : [],
452             'html' => $includehtml ? [
453                 'rating' => ($loadcontent && $hasrating) ? $output->render($rating) : null,
454                 'taglist' => ($loadcontent && $hastags) ? $output->tag_list($tags) : null,
455                 'authorsubheading' => ($loadcontent) ? $this->get_author_subheading_html($exportedauthor, $timecreated) : null
456             ] : null
457         ];
458     }
460     /**
461      * Returns a list of objects that are related.
462      *
463      * @return array
464      */
465     protected static function define_related() {
466         return [
467             'capabilitymanager' => 'mod_forum\local\managers\capability',
468             'readreceiptcollection' => 'mod_forum\local\entities\post_read_receipt_collection?',
469             'urlfactory' => 'mod_forum\local\factories\url',
470             'forum' => 'mod_forum\local\entities\forum',
471             'discussion' => 'mod_forum\local\entities\discussion',
472             'author' => 'mod_forum\local\entities\author',
473             'authorcontextid' => 'int?',
474             'user' => 'stdClass',
475             'context' => 'context',
476             'authorgroups' => 'stdClass[]',
477             'attachments' => '\stored_file[]?',
478             'tags' => '\core_tag_tag[]?',
479             'rating' => 'rating?',
480             'includehtml' => 'bool'
481         ];
482     }
484     /**
485      * This method returns the parameters for the post's message to
486      * use with the function external_format_text().
487      *
488      * @return array
489      */
490     protected function get_format_parameters_for_message() {
491         return [
492             'component' => 'mod_forum',
493             'filearea' => 'post',
494             'itemid' => $this->post->get_id(),
495             'options' => [
496                 'para' => false,
497                 'trusted' => $this->post->is_message_trusted()
498             ]
499         ];
500     }
502     /**
503      * Get the message text from a post.
504      *
505      * @param post_entity $post The post
506      * @return string
507      */
508     private function get_message(post_entity $post) : string {
509         global $CFG;
511         $message = $post->get_message();
513         if (!empty($CFG->enableplagiarism)) {
514             require_once($CFG->libdir . '/plagiarismlib.php');
515             $forum = $this->related['forum'];
516             $message .= plagiarism_get_links([
517                 'userid' => $post->get_author_id(),
518                 'content' => $message,
519                 'cmid' => $forum->get_course_module_record()->id,
520                 'course' => $forum->get_course_id(),
521                 'forum' => $forum->get_id()
522             ]);
523         }
525         return $message;
526     }
528     /**
529      * Get the exported attachments for a post.
530      *
531      * @param stored_file[] $attachments The list of attachments for the post
532      * @param post_entity $post The post being exported
533      * @param renderer_base $output Renderer base
534      * @param bool $canexport If the user can export the post (relates to portfolios not exporters like this class)
535      * @return array
536      */
537     private function export_attachments(array $attachments, post_entity $post, renderer_base $output, bool $canexport) : array {
538         global $CFG;
540         $urlfactory = $this->related['urlfactory'];
541         $enableplagiarism = $CFG->enableplagiarism;
542         $forum = $this->related['forum'];
543         $context = $this->related['context'];
545         if ($enableplagiarism) {
546             require_once($CFG->libdir . '/plagiarismlib.php' );
547         }
549         return array_map(function($attachment) use (
550             $output,
551             $enableplagiarism,
552             $canexport,
553             $context,
554             $forum,
555             $post,
556             $urlfactory
557         ) {
558             $exporter = new stored_file_exporter($attachment, ['context' => $context]);
559             $exportedattachment = $exporter->export($output);
560             $exporturl = $canexport ? $urlfactory->get_export_attachment_url_from_post_and_attachment($post, $attachment) : null;
562             if ($enableplagiarism) {
563                 $plagiarismhtml = plagiarism_get_links([
564                     'userid' => $post->get_author_id(),
565                     'file' => $attachment,
566                     'cmid' => $forum->get_course_module_record()->id,
567                     'course' => $forum->get_course_id(),
568                     'forum' => $forum->get_id()
569                 ]);
570             } else {
571                 $plagiarismhtml = null;
572             }
574             $exportedattachment->urls = [
575                 'export' => $exporturl ? $exporturl->out(false) : null
576             ];
577             $exportedattachment->html = [
578                 'plagiarism' => $plagiarismhtml
579             ];
581             return $exportedattachment;
582         }, $attachments);
583     }
585     /**
586      * Export the list of tags.
587      *
588      * @param core_tag_tag[] $tags List of tags to export
589      * @return array
590      */
591     private function export_tags(array $tags) : array {
592         $user = $this->related['user'];
593         $context = $this->related['context'];
594         $capabilitymanager = $this->related['capabilitymanager'];
595         $canmanagetags = $capabilitymanager->can_manage_tags($user);
597         return array_values(array_map(function($tag) use ($context, $canmanagetags) {
598             $viewurl = core_tag_tag::make_url($tag->tagcollid, $tag->rawname, 0, $context->id);
599             return [
600                 'id' => $tag->taginstanceid,
601                 'tagid' => $tag->id,
602                 'isstandard' => $tag->isstandard,
603                 'displayname' => $tag->get_display_name(),
604                 'flag' => $canmanagetags && !empty($tag->flag),
605                 'urls' => [
606                     'view' => $viewurl->out(false)
607                 ]
608             ];
609         }, $tags));
610     }
612     /**
613      * Get the HTML to display as a subheading in a post.
614      *
615      * @param stdClass $exportedauthor The exported author object
616      * @param int $timecreated The post time created timestamp if it's to be displayed
617      * @return string
618      */
619     private function get_author_subheading_html(stdClass $exportedauthor, int $timecreated) : string {
620         $fullname = $exportedauthor->fullname;
621         $profileurl = $exportedauthor->urls['profile'] ?? null;
622         $formatteddate = userdate($timecreated, get_string('strftimedaydatetime', 'core_langconfig'));
623         $name = $profileurl ? "<a href=\"{$profileurl}\">{$fullname}</a>" : $fullname;
624         $date = "<time>{$formatteddate}</time>";
625         return get_string('bynameondate', 'mod_forum', ['name' => $name, 'date' => $date]);
626     }