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