Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 401 and 402] [Versions 401 and 403]

   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/>.
  16  
  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   */
  24  
  25  namespace mod_forum\local\exporters;
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  use mod_forum\local\entities\post as post_entity;
  30  use mod_forum\local\entities\discussion as discussion_entity;
  31  use mod_forum\local\exporters\author as author_exporter;
  32  use mod_forum\local\factories\exporter as exporter_factory;
  33  use core\external\exporter;
  34  use core_files\external\stored_file_exporter;
  35  use context;
  36  use core_tag_tag;
  37  use renderer_base;
  38  use stdClass;
  39  
  40  require_once($CFG->dirroot . '/mod/forum/lib.php');
  41  
  42  /**
  43   * Post exporter class.
  44   *
  45   * @copyright  2019 Ryan Wyllie <ryan@moodle.com>
  46   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  47   */
  48  class post extends exporter {
  49      /** @var post_entity $post The post to export */
  50      private $post;
  51  
  52      /**
  53       * Constructor.
  54       *
  55       * @param post_entity $post The post to export
  56       * @param array $related List of related data
  57       */
  58      public function __construct(post_entity $post, array $related = []) {
  59          $this->post = $post;
  60          return parent::__construct([], $related);
  61      }
  62  
  63      /**
  64       * Return the list of additional properties.
  65       *
  66       * @return array
  67       */
  68      protected static function define_other_properties() {
  69          $attachmentdefinition = stored_file_exporter::read_properties_definition();
  70          $attachmentdefinition['urls'] = [
  71              'type' => [
  72                  'export' => [
  73                      'type' => PARAM_URL,
  74                      'description' => 'The URL used to export the attachment',
  75                      'optional' => true,
  76                      'default' => null,
  77                      'null' => NULL_ALLOWED
  78                  ]
  79              ]
  80          ];
  81          $attachmentdefinition['html'] = [
  82              'type' => [
  83                  'plagiarism' => [
  84                      'type' => PARAM_RAW,
  85                      'description' => 'The HTML source for the Plagiarism Response',
  86                      'optional' => true,
  87                      'default' => null,
  88                      'null' => NULL_ALLOWED
  89                  ],
  90              ]
  91          ];
  92  
  93          return [
  94              'id' => ['type' => PARAM_INT],
  95              'subject' => ['type' => PARAM_TEXT],
  96              'replysubject' => ['type' => PARAM_TEXT],
  97              'message' => ['type' => PARAM_RAW],
  98              'messageformat' => ['type' => PARAM_INT],
  99              'author' => ['type' => author_exporter::read_properties_definition()],
 100              'discussionid' => ['type' => PARAM_INT],
 101              'hasparent' => ['type' => PARAM_BOOL],
 102              'parentid' => [
 103                  'type' => PARAM_INT,
 104                  'optional' => true,
 105                  'default' => null,
 106                  'null' => NULL_ALLOWED
 107              ],
 108              'timecreated' => [
 109                  'type' => PARAM_INT,
 110                  'default' => null,
 111                  'null' => NULL_ALLOWED
 112              ],
 113              'timemodified' => [
 114                  'type' => PARAM_INT,
 115                  'default' => null,
 116                  'null' => NULL_ALLOWED
 117              ],
 118              'unread' => [
 119                  'type' => PARAM_BOOL,
 120                  'optional' => true,
 121                  'default' => null,
 122                  'null' => NULL_ALLOWED
 123              ],
 124              'isdeleted' => ['type' => PARAM_BOOL],
 125              'isprivatereply' => ['type' => PARAM_BOOL],
 126              'haswordcount' => ['type' => PARAM_BOOL],
 127              'wordcount' => [
 128                  'type' => PARAM_INT,
 129                  'optional' => true,
 130                  'default' => null,
 131                  'null' => NULL_ALLOWED
 132              ],
 133              'charcount' => [
 134                  'type' => PARAM_INT,
 135                  'optional' => true,
 136                  'default' => null,
 137                  'null' => NULL_ALLOWED
 138              ],
 139              'capabilities' => [
 140                  'type' => [
 141                      'view' => [
 142                          'type' => PARAM_BOOL,
 143                          'null' => NULL_ALLOWED,
 144                          'description' => 'Whether the user can view the post',
 145                      ],
 146                      'edit' => [
 147                          'type' => PARAM_BOOL,
 148                          'null' => NULL_ALLOWED,
 149                          'description' => 'Whether the user can edit the post',
 150                      ],
 151                      'delete' => [
 152                          'type' => PARAM_BOOL,
 153                          'null' => NULL_ALLOWED,
 154                          'description' => 'Whether the user can delete the post',
 155                      ],
 156                      'split' => [
 157                          'type' => PARAM_BOOL,
 158                          'null' => NULL_ALLOWED,
 159                          'description' => 'Whether the user can split the post',
 160                      ],
 161                      'reply' => [
 162                          'type' => PARAM_BOOL,
 163                          'null' => NULL_ALLOWED,
 164                          'description' => 'Whether the user can reply to the post',
 165                      ],
 166                      'selfenrol' => [
 167                          'type' => PARAM_BOOL,
 168                          'null' => NULL_ALLOWED,
 169                          'description' => 'Whether the user can self enrol into the course',
 170                      ],
 171                      'export' => [
 172                          'type' => PARAM_BOOL,
 173                          'null' => NULL_ALLOWED,
 174                          'description' => 'Whether the user can export the post',
 175                      ],
 176                      'controlreadstatus' => [
 177                          'type' => PARAM_BOOL,
 178                          'null' => NULL_ALLOWED,
 179                          'description' => 'Whether the user can control the read status of the post',
 180                      ],
 181                      'canreplyprivately' => [
 182                          'type' => PARAM_BOOL,
 183                          'null' => NULL_ALLOWED,
 184                          'description' => 'Whether the user can post a private reply',
 185                      ]
 186                  ]
 187              ],
 188              'urls' => [
 189                  'optional' => true,
 190                  'default' => null,
 191                  'null' => NULL_ALLOWED,
 192                  'type' => [
 193                      'view' => [
 194                          'description' => 'The URL used to view the post',
 195                          'type' => PARAM_URL,
 196                          'optional' => true,
 197                          'default' => null,
 198                          'null' => NULL_ALLOWED
 199                      ],
 200                      'viewisolated' => [
 201                          'description' => 'The URL used to view the post in isolation',
 202                          'type' => PARAM_URL,
 203                          'optional' => true,
 204                          'default' => null,
 205                          'null' => NULL_ALLOWED
 206                      ],
 207                      'viewparent' => [
 208                          'description' => 'The URL used to view the parent of the post',
 209                          'type' => PARAM_URL,
 210                          'optional' => true,
 211                          'default' => null,
 212                          'null' => NULL_ALLOWED
 213                      ],
 214                      'edit' => [
 215                          'description' => 'The URL used to edit the post',
 216                          'type' => PARAM_URL,
 217                          'optional' => true,
 218                          'default' => null,
 219                          'null' => NULL_ALLOWED
 220                      ],
 221                      'delete' => [
 222                          'description' => 'The URL used to delete the post',
 223                          'type' => PARAM_URL,
 224                          'optional' => true,
 225                          'default' => null,
 226                          'null' => NULL_ALLOWED
 227                      ],
 228                      'split' => [
 229                          'description' => 'The URL used to split the discussion ' .
 230                              'with the selected post being the first post in the new discussion',
 231                          'type' => PARAM_URL,
 232                          'optional' => true,
 233                          'default' => null,
 234                          'null' => NULL_ALLOWED
 235                      ],
 236                      'reply' => [
 237                          'description' => 'The URL used to reply to the post',
 238                          'type' => PARAM_URL,
 239                          'optional' => true,
 240                          'default' => null,
 241                          'null' => NULL_ALLOWED
 242                      ],
 243                      'export' => [
 244                          'description' => 'The URL used to export the post',
 245                          'type' => PARAM_URL,
 246                          'optional' => true,
 247                          'default' => null,
 248                          'null' => NULL_ALLOWED
 249                      ],
 250                      'markasread' => [
 251                          'description' => 'The URL used to mark the post as read',
 252                          'type' => PARAM_URL,
 253                          'optional' => true,
 254                          'default' => null,
 255                          'null' => NULL_ALLOWED
 256                      ],
 257                      'markasunread' => [
 258                          'description' => 'The URL used to mark the post as unread',
 259                          'type' => PARAM_URL,
 260                          'optional' => true,
 261                          'default' => null,
 262                          'null' => NULL_ALLOWED
 263                      ],
 264                      'discuss' => [
 265                          'type' => PARAM_URL,
 266                          'optional' => true,
 267                          'default' => null,
 268                          'null' => NULL_ALLOWED
 269                      ]
 270                  ]
 271              ],
 272              'attachments' => [
 273                  'multiple' => true,
 274                  'type' => $attachmentdefinition
 275              ],
 276              'messageinlinefiles' => [
 277                  'optional' => true,
 278                  'multiple' => true,
 279                  'type' => stored_file_exporter::read_properties_definition(),
 280              ],
 281              'tags' => [
 282                  'optional' => true,
 283                  'default' => null,
 284                  'null' => NULL_ALLOWED,
 285                  'multiple' => true,
 286                  'type' => [
 287                      'id' => [
 288                          'type' => PARAM_INT,
 289                          'description' => 'The ID of the Tag',
 290                          'null' => NULL_NOT_ALLOWED,
 291                      ],
 292                      'tagid' => [
 293                          'type' => PARAM_INT,
 294                          'description' => 'The tagid',
 295                          'null' => NULL_NOT_ALLOWED,
 296                      ],
 297                      'isstandard' => [
 298                          'type' => PARAM_BOOL,
 299                          'description' => 'Whether this is a standard tag',
 300                          'null' => NULL_NOT_ALLOWED,
 301                      ],
 302                      'displayname' => [
 303                          'type' => PARAM_TEXT,
 304                          'description' => 'The display name of the tag',
 305                          'null' => NULL_NOT_ALLOWED,
 306                      ],
 307                      'flag' => [
 308                          'type' => PARAM_BOOL,
 309                          'description' => 'Wehther this tag is flagged',
 310                          'null' => NULL_NOT_ALLOWED,
 311                      ],
 312                      'urls' => [
 313                          'description' => 'URLs associated with the tag',
 314                          'null' => NULL_NOT_ALLOWED,
 315                          'type' => [
 316                              'view' => [
 317                                  'type' => PARAM_URL,
 318                                  'description' => 'The URL to view the tag',
 319                                  'null' => NULL_NOT_ALLOWED,
 320                              ],
 321                          ]
 322                      ]
 323                  ]
 324              ],
 325              'html' => [
 326                  'optional' => true,
 327                  'default' => null,
 328                  'null' => NULL_ALLOWED,
 329                  'type' => [
 330                      'rating' => [
 331                          'optional' => true,
 332                          'default' => null,
 333                          'null' => NULL_ALLOWED,
 334                          'type' => PARAM_RAW,
 335                          'description' => 'The HTML source to rate the post',
 336                      ],
 337                      'taglist' => [
 338                          'optional' => true,
 339                          'default' => null,
 340                          'null' => NULL_ALLOWED,
 341                          'type' => PARAM_RAW,
 342                          'description' => 'The HTML source to view the list of tags',
 343                      ],
 344                      'authorsubheading' => [
 345                          'optional' => true,
 346                          'default' => null,
 347                          'null' => NULL_ALLOWED,
 348                          'type' => PARAM_RAW,
 349                          'description' => 'The HTML source to view the author details',
 350                      ],
 351                  ]
 352              ]
 353          ];
 354      }
 355  
 356      /**
 357       * Get the additional values to inject while exporting.
 358       *
 359       * @param renderer_base $output The renderer.
 360       * @return array Keys are the property names, values are their values.
 361       */
 362      protected function get_other_values(renderer_base $output) {
 363          $post = $this->post;
 364          $authorgroups = $this->related['authorgroups'];
 365          $forum = $this->related['forum'];
 366          $discussion = $this->related['discussion'];
 367          $author = $this->related['author'];
 368          $authorcontextid = $this->related['authorcontextid'];
 369          $user = $this->related['user'];
 370          $readreceiptcollection = $this->related['readreceiptcollection'];
 371          $rating = $this->related['rating'];
 372          $tags = $this->related['tags'];
 373          $attachments = $this->related['attachments'];
 374          $inlineattachments = $this->related['messageinlinefiles'];
 375          $includehtml = $this->related['includehtml'];
 376          $isdeleted = $post->is_deleted();
 377          $isprivatereply = $post->is_private_reply();
 378          $hasrating = $rating != null;
 379          $hastags = !empty($tags);
 380          $discussionid = $post->get_discussion_id();
 381          $parentid = $post->get_parent_id();
 382  
 383          $capabilitymanager = $this->related['capabilitymanager'];
 384          $canview = $capabilitymanager->can_view_post($user, $discussion, $post);
 385          $canedit = $capabilitymanager->can_edit_post($user, $discussion, $post);
 386          $candelete = $capabilitymanager->can_delete_post($user, $discussion, $post);
 387          $cansplit = $capabilitymanager->can_split_post($user, $discussion, $post);
 388          $canreply = $capabilitymanager->can_reply_to_post($user, $discussion, $post);
 389          $canexport = $capabilitymanager->can_export_post($user, $post);
 390          $cancontrolreadstatus = $capabilitymanager->can_manually_control_post_read_status($user);
 391          $canselfenrol = $capabilitymanager->can_self_enrol($user);
 392          $canreplyprivately = $capabilitymanager->can_reply_privately_to_post($user, $post);
 393  
 394          $urlfactory = $this->related['urlfactory'];
 395          $viewurl = $canview ? $urlfactory->get_view_post_url_from_post($post) : null;
 396          $viewisolatedurl = $canview ? $urlfactory->get_view_isolated_post_url_from_post($post) : null;
 397          $viewparenturl = $post->has_parent() ? $urlfactory->get_view_post_url_from_post_id($discussionid, $parentid) : null;
 398          $editurl = $canedit ? $urlfactory->get_edit_post_url_from_post($forum, $post) : null;
 399          $deleteurl = $candelete ? $urlfactory->get_delete_post_url_from_post($post) : null;
 400          $spliturl = $cansplit ? $urlfactory->get_split_discussion_at_post_url_from_post($post) : null;
 401          $replyurl = $canreply || $canselfenrol ? $urlfactory->get_reply_to_post_url_from_post($post) : null;
 402          $exporturl = $canexport ? $urlfactory->get_export_post_url_from_post($post) : null;
 403          $markasreadurl = $cancontrolreadstatus ? $urlfactory->get_mark_post_as_read_url_from_post($post) : null;
 404          $markasunreadurl = $cancontrolreadstatus ? $urlfactory->get_mark_post_as_unread_url_from_post($post) : null;
 405          $discussurl = $canview ? $urlfactory->get_discussion_view_url_from_post($post) : null;
 406  
 407          $authorexporter = new author_exporter(
 408              $author,
 409              $authorcontextid,
 410              $authorgroups,
 411              $canview,
 412              $this->related
 413          );
 414          $exportedauthor = $authorexporter->export($output);
 415          // Only bother loading the content if the user can see it.
 416          $loadcontent = $canview && !$isdeleted;
 417          $exportattachments = $loadcontent && !empty($attachments);
 418          $exportinlineattachments = $loadcontent && !empty($inlineattachments);
 419  
 420          if ($loadcontent) {
 421              $subject = $post->get_subject();
 422              $timecreated = $this->get_start_time($discussion, $post);
 423              $message = $this->get_message($post);
 424          } else {
 425              $subject = $isdeleted ? get_string('forumsubjectdeleted', 'forum') : get_string('forumsubjecthidden', 'forum');
 426              $message = $isdeleted ? get_string('forumbodydeleted', 'forum') : get_string('forumbodyhidden', 'forum');
 427              $timecreated = null;
 428          }
 429  
 430          $replysubject = $subject;
 431          $strre = get_string('re', 'forum');
 432          if (!(substr($replysubject, 0, strlen($strre)) == $strre)) {
 433              $replysubject = "{$strre} {$replysubject}";
 434          }
 435  
 436          $showwordcount = $forum->should_display_word_count();
 437          if ($showwordcount) {
 438              $wordcount = $post->get_wordcount() ?? count_words($message);
 439              $charcount = $post->get_charcount() ?? count_letters($message);
 440          } else {
 441              $wordcount = null;
 442              $charcount = null;
 443          }
 444  
 445          return [
 446              'id' => $post->get_id(),
 447              'subject' => $subject,
 448              'replysubject' => $replysubject,
 449              'message' => $message,
 450              'messageformat' => $post->get_message_format(),
 451              'author' => $exportedauthor,
 452              'discussionid' => $post->get_discussion_id(),
 453              'hasparent' => $post->has_parent(),
 454              'parentid' => $post->has_parent() ? $post->get_parent_id() : null,
 455              'timecreated' => $timecreated,
 456              'timemodified' => $post->get_time_modified(),
 457              'unread' => ($loadcontent && $readreceiptcollection) ? !$readreceiptcollection->has_user_read_post($user, $post) : null,
 458              'isdeleted' => $isdeleted,
 459              'isprivatereply' => $isprivatereply,
 460              'haswordcount' => $showwordcount,
 461              'wordcount' => $wordcount,
 462              'charcount' => $charcount,
 463              'capabilities' => [
 464                  'view' => $canview,
 465                  'edit' => $canedit,
 466                  'delete' => $candelete,
 467                  'split' => $cansplit,
 468                  'reply' => $canreply,
 469                  'export' => $canexport,
 470                  'controlreadstatus' => $cancontrolreadstatus,
 471                  'canreplyprivately' => $canreplyprivately,
 472                  'selfenrol' => $canselfenrol
 473              ],
 474              'urls' => [
 475                  'view' => $viewurl ? $viewurl->out(false) : null,
 476                  'viewisolated' => $viewisolatedurl ? $viewisolatedurl->out(false) : null,
 477                  'viewparent' => $viewparenturl ? $viewparenturl->out(false) : null,
 478                  'edit' => $editurl ? $editurl->out(false) : null,
 479                  'delete' => $deleteurl ? $deleteurl->out(false) : null,
 480                  'split' => $spliturl ? $spliturl->out(false) : null,
 481                  'reply' => $replyurl ? $replyurl->out(false) : null,
 482                  'export' => $exporturl && $exporturl ? $exporturl->out(false) : null,
 483                  'markasread' => $markasreadurl ? $markasreadurl->out(false) : null,
 484                  'markasunread' => $markasunreadurl ? $markasunreadurl->out(false) : null,
 485                  'discuss' => $discussurl ? $discussurl->out(false) : null,
 486              ],
 487              'attachments' => ($exportattachments) ? $this->export_attachments($attachments, $post, $output, $canexport) : [],
 488              'messageinlinefiles' => ($exportinlineattachments) ? $this->export_inline_attachments($inlineattachments,
 489                  $post, $output) : [],
 490              'tags' => ($loadcontent && $hastags) ? $this->export_tags($tags) : [],
 491              'html' => $includehtml ? [
 492                  'rating' => ($loadcontent && $hasrating) ? $output->render($rating) : null,
 493                  'taglist' => ($loadcontent && $hastags) ? $output->tag_list($tags) : null,
 494                  'authorsubheading' => ($loadcontent) ? $this->get_author_subheading_html($exportedauthor, $timecreated) : null
 495              ] : null
 496          ];
 497      }
 498  
 499      /**
 500       * Returns a list of objects that are related.
 501       *
 502       * @return array
 503       */
 504      protected static function define_related() {
 505          return [
 506              'capabilitymanager' => 'mod_forum\local\managers\capability',
 507              'readreceiptcollection' => 'mod_forum\local\entities\post_read_receipt_collection?',
 508              'urlfactory' => 'mod_forum\local\factories\url',
 509              'forum' => 'mod_forum\local\entities\forum',
 510              'discussion' => 'mod_forum\local\entities\discussion',
 511              'author' => 'mod_forum\local\entities\author',
 512              'authorcontextid' => 'int?',
 513              'user' => 'stdClass',
 514              'context' => 'context',
 515              'authorgroups' => 'stdClass[]',
 516              'attachments' => '\stored_file[]?',
 517              'messageinlinefiles' => '\stored_file[]?',
 518              'tags' => '\core_tag_tag[]?',
 519              'rating' => 'rating?',
 520              'includehtml' => 'bool'
 521          ];
 522      }
 523  
 524      /**
 525       * This method returns the parameters for the post's message to
 526       * use with the function external_format_text().
 527       *
 528       * @return array
 529       */
 530      protected function get_format_parameters_for_message() {
 531          return [
 532              'component' => 'mod_forum',
 533              'filearea' => 'post',
 534              'itemid' => $this->post->get_id(),
 535              'options' => [
 536                  'para' => false,
 537                  'trusted' => $this->post->is_message_trusted()
 538              ]
 539          ];
 540      }
 541  
 542      /**
 543       * Get the message text from a post.
 544       *
 545       * @param post_entity $post The post
 546       * @return string
 547       */
 548      private function get_message(post_entity $post) : string {
 549          global $CFG;
 550  
 551          $message = $post->get_message();
 552  
 553          if (!empty($CFG->enableplagiarism)) {
 554              require_once($CFG->libdir . '/plagiarismlib.php');
 555              $forum = $this->related['forum'];
 556              $message .= plagiarism_get_links([
 557                  'userid' => $post->get_author_id(),
 558                  'content' => $message,
 559                  'cmid' => $forum->get_course_module_record()->id,
 560                  'course' => $forum->get_course_id(),
 561                  'forum' => $forum->get_id()
 562              ]);
 563          }
 564  
 565          return $message;
 566      }
 567  
 568      /**
 569       * Get the exported attachments for a post.
 570       *
 571       * @param stored_file[] $attachments The list of attachments for the post
 572       * @param post_entity $post The post being exported
 573       * @param renderer_base $output Renderer base
 574       * @param bool $canexport If the user can export the post (relates to portfolios not exporters like this class)
 575       * @return array
 576       */
 577      private function export_attachments(array $attachments, post_entity $post, renderer_base $output, bool $canexport) : array {
 578          global $CFG;
 579  
 580          $urlfactory = $this->related['urlfactory'];
 581          $enableplagiarism = $CFG->enableplagiarism;
 582          $forum = $this->related['forum'];
 583          $context = $this->related['context'];
 584  
 585          if ($enableplagiarism) {
 586              require_once($CFG->libdir . '/plagiarismlib.php' );
 587          }
 588  
 589          return array_map(function($attachment) use (
 590              $output,
 591              $enableplagiarism,
 592              $canexport,
 593              $context,
 594              $forum,
 595              $post,
 596              $urlfactory
 597          ) {
 598              $exporter = new stored_file_exporter($attachment, ['context' => $context]);
 599              $exportedattachment = $exporter->export($output);
 600              $exporturl = $canexport ? $urlfactory->get_export_attachment_url_from_post_and_attachment($post, $attachment) : null;
 601  
 602              if ($enableplagiarism) {
 603                  $plagiarismhtml = plagiarism_get_links([
 604                      'userid' => $post->get_author_id(),
 605                      'file' => $attachment,
 606                      'cmid' => $forum->get_course_module_record()->id,
 607                      'course' => $forum->get_course_id(),
 608                      'forum' => $forum->get_id()
 609                  ]);
 610              } else {
 611                  $plagiarismhtml = null;
 612              }
 613  
 614              $exportedattachment->urls = [
 615                  'export' => $exporturl ? $exporturl->out(false) : null
 616              ];
 617              $exportedattachment->html = [
 618                  'plagiarism' => $plagiarismhtml
 619              ];
 620  
 621              return $exportedattachment;
 622          }, $attachments);
 623      }
 624  
 625      /**
 626       * Get the exported inline attachments for a post.
 627       *
 628       * @param array $inlineattachments The list of inline attachments for the post
 629       * @param post_entity $post The post being exported
 630       * @param renderer_base $output Renderer base
 631       * @return array
 632       */
 633      private function export_inline_attachments(array $inlineattachments, post_entity $post, renderer_base $output) : array {
 634  
 635          return array_map(function($attachment) use (
 636              $output,
 637              $post
 638          ) {
 639              $exporter = new stored_file_exporter($attachment, ['context' => $this->related['context']]);
 640              return $exporter->export($output);;
 641          }, $inlineattachments);
 642      }
 643  
 644      /**
 645       * Export the list of tags.
 646       *
 647       * @param core_tag_tag[] $tags List of tags to export
 648       * @return array
 649       */
 650      private function export_tags(array $tags) : array {
 651          $user = $this->related['user'];
 652          $context = $this->related['context'];
 653          $capabilitymanager = $this->related['capabilitymanager'];
 654          $canmanagetags = $capabilitymanager->can_manage_tags($user);
 655  
 656          return array_values(array_map(function($tag) use ($context, $canmanagetags) {
 657              $viewurl = core_tag_tag::make_url($tag->tagcollid, $tag->rawname, 0, $context->id);
 658              return [
 659                  'id' => $tag->taginstanceid,
 660                  'tagid' => $tag->id,
 661                  'isstandard' => $tag->isstandard,
 662                  'displayname' => $tag->get_display_name(),
 663                  'flag' => $canmanagetags && !empty($tag->flag),
 664                  'urls' => [
 665                      'view' => $viewurl->out(false)
 666                  ]
 667              ];
 668          }, $tags));
 669      }
 670  
 671      /**
 672       * Get the HTML to display as a subheading in a post.
 673       *
 674       * @param stdClass $exportedauthor The exported author object
 675       * @param int $timecreated The post time created timestamp if it's to be displayed
 676       * @return string
 677       */
 678      private function get_author_subheading_html(stdClass $exportedauthor, int $timecreated) : string {
 679          $fullname = $exportedauthor->fullname;
 680          $profileurl = $exportedauthor->urls['profile'] ?? null;
 681          $name = $profileurl ? "<a href=\"{$profileurl}\">{$fullname}</a>" : $fullname;
 682          $date = userdate_htmltime($timecreated, get_string('strftimedaydatetime', 'core_langconfig'));
 683          return get_string('bynameondate', 'mod_forum', ['name' => $name, 'date' => $date]);
 684      }
 685  
 686      /**
 687       * Get the start time for a post.
 688       *
 689       * @param discussion_entity $discussion entity
 690       * @param post_entity $post entity
 691       * @return int The start time (timestamp) for a post
 692       */
 693      private function get_start_time(discussion_entity $discussion, post_entity $post) {
 694          global $CFG;
 695  
 696          $posttime = $post->get_time_created();
 697          $discussiontime = $discussion->get_time_start();
 698          if (!empty($CFG->forum_enabletimedposts) && ($discussiontime > $posttime)) {
 699              return $discussiontime;
 700          }
 701          return $posttime;
 702      }
 703  }