Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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

   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              'tags' => [
 277                  'optional' => true,
 278                  'default' => null,
 279                  'null' => NULL_ALLOWED,
 280                  'multiple' => true,
 281                  'type' => [
 282                      'id' => [
 283                          'type' => PARAM_INT,
 284                          'description' => 'The ID of the Tag',
 285                          'null' => NULL_NOT_ALLOWED,
 286                      ],
 287                      'tagid' => [
 288                          'type' => PARAM_INT,
 289                          'description' => 'The tagid',
 290                          'null' => NULL_NOT_ALLOWED,
 291                      ],
 292                      'isstandard' => [
 293                          'type' => PARAM_BOOL,
 294                          'description' => 'Whether this is a standard tag',
 295                          'null' => NULL_NOT_ALLOWED,
 296                      ],
 297                      'displayname' => [
 298                          'type' => PARAM_TEXT,
 299                          'description' => 'The display name of the tag',
 300                          'null' => NULL_NOT_ALLOWED,
 301                      ],
 302                      'flag' => [
 303                          'type' => PARAM_BOOL,
 304                          'description' => 'Wehther this tag is flagged',
 305                          'null' => NULL_NOT_ALLOWED,
 306                      ],
 307                      'urls' => [
 308                          'description' => 'URLs associated with the tag',
 309                          'null' => NULL_NOT_ALLOWED,
 310                          'type' => [
 311                              'view' => [
 312                                  'type' => PARAM_URL,
 313                                  'description' => 'The URL to view the tag',
 314                                  'null' => NULL_NOT_ALLOWED,
 315                              ],
 316                          ]
 317                      ]
 318                  ]
 319              ],
 320              'html' => [
 321                  'optional' => true,
 322                  'default' => null,
 323                  'null' => NULL_ALLOWED,
 324                  'type' => [
 325                      'rating' => [
 326                          'optional' => true,
 327                          'default' => null,
 328                          'null' => NULL_ALLOWED,
 329                          'type' => PARAM_RAW,
 330                          'description' => 'The HTML source to rate the post',
 331                      ],
 332                      'taglist' => [
 333                          'optional' => true,
 334                          'default' => null,
 335                          'null' => NULL_ALLOWED,
 336                          'type' => PARAM_RAW,
 337                          'description' => 'The HTML source to view the list of tags',
 338                      ],
 339                      'authorsubheading' => [
 340                          'optional' => true,
 341                          'default' => null,
 342                          'null' => NULL_ALLOWED,
 343                          'type' => PARAM_RAW,
 344                          'description' => 'The HTML source to view the author details',
 345                      ],
 346                  ]
 347              ]
 348          ];
 349      }
 350  
 351      /**
 352       * Get the additional values to inject while exporting.
 353       *
 354       * @param renderer_base $output The renderer.
 355       * @return array Keys are the property names, values are their values.
 356       */
 357      protected function get_other_values(renderer_base $output) {
 358          $post = $this->post;
 359          $authorgroups = $this->related['authorgroups'];
 360          $forum = $this->related['forum'];
 361          $discussion = $this->related['discussion'];
 362          $author = $this->related['author'];
 363          $authorcontextid = $this->related['authorcontextid'];
 364          $user = $this->related['user'];
 365          $readreceiptcollection = $this->related['readreceiptcollection'];
 366          $rating = $this->related['rating'];
 367          $tags = $this->related['tags'];
 368          $attachments = $this->related['attachments'];
 369          $includehtml = $this->related['includehtml'];
 370          $isdeleted = $post->is_deleted();
 371          $isprivatereply = $post->is_private_reply();
 372          $hasrating = $rating != null;
 373          $hastags = !empty($tags);
 374          $discussionid = $post->get_discussion_id();
 375          $parentid = $post->get_parent_id();
 376  
 377          $capabilitymanager = $this->related['capabilitymanager'];
 378          $canview = $capabilitymanager->can_view_post($user, $discussion, $post);
 379          $canedit = $capabilitymanager->can_edit_post($user, $discussion, $post);
 380          $candelete = $capabilitymanager->can_delete_post($user, $discussion, $post);
 381          $cansplit = $capabilitymanager->can_split_post($user, $discussion, $post);
 382          $canreply = $capabilitymanager->can_reply_to_post($user, $discussion, $post);
 383          $canexport = $capabilitymanager->can_export_post($user, $post);
 384          $cancontrolreadstatus = $capabilitymanager->can_manually_control_post_read_status($user);
 385          $canselfenrol = $capabilitymanager->can_self_enrol($user);
 386          $canreplyprivately = $capabilitymanager->can_reply_privately_to_post($user, $post);
 387  
 388          $urlfactory = $this->related['urlfactory'];
 389          $viewurl = $canview ? $urlfactory->get_view_post_url_from_post($post) : null;
 390          $viewisolatedurl = $canview ? $urlfactory->get_view_isolated_post_url_from_post($post) : null;
 391          $viewparenturl = $post->has_parent() ? $urlfactory->get_view_post_url_from_post_id($discussionid, $parentid) : null;
 392          $editurl = $canedit ? $urlfactory->get_edit_post_url_from_post($forum, $post) : null;
 393          $deleteurl = $candelete ? $urlfactory->get_delete_post_url_from_post($post) : null;
 394          $spliturl = $cansplit ? $urlfactory->get_split_discussion_at_post_url_from_post($post) : null;
 395          $replyurl = $canreply || $canselfenrol ? $urlfactory->get_reply_to_post_url_from_post($post) : null;
 396          $exporturl = $canexport ? $urlfactory->get_export_post_url_from_post($post) : null;
 397          $markasreadurl = $cancontrolreadstatus ? $urlfactory->get_mark_post_as_read_url_from_post($post) : null;
 398          $markasunreadurl = $cancontrolreadstatus ? $urlfactory->get_mark_post_as_unread_url_from_post($post) : null;
 399          $discussurl = $canview ? $urlfactory->get_discussion_view_url_from_post($post) : null;
 400  
 401          $authorexporter = new author_exporter(
 402              $author,
 403              $authorcontextid,
 404              $authorgroups,
 405              $canview,
 406              $this->related
 407          );
 408          $exportedauthor = $authorexporter->export($output);
 409          // Only bother loading the content if the user can see it.
 410          $loadcontent = $canview && !$isdeleted;
 411          $exportattachments = $loadcontent && !empty($attachments);
 412  
 413          if ($loadcontent) {
 414              $subject = $post->get_subject();
 415              $timecreated = $this->get_start_time($discussion, $post);
 416              $message = $this->get_message($post);
 417          } else {
 418              $subject = $isdeleted ? get_string('forumsubjectdeleted', 'forum') : get_string('forumsubjecthidden', 'forum');
 419              $message = $isdeleted ? get_string('forumbodydeleted', 'forum') : get_string('forumbodyhidden', 'forum');
 420              $timecreated = null;
 421          }
 422  
 423          $replysubject = $subject;
 424          $strre = get_string('re', 'forum');
 425          if (!(substr($replysubject, 0, strlen($strre)) == $strre)) {
 426              $replysubject = "{$strre} {$replysubject}";
 427          }
 428  
 429          $showwordcount = $forum->should_display_word_count();
 430          if ($showwordcount) {
 431              $wordcount = $post->get_wordcount() ?? count_words($message);
 432              $charcount = $post->get_charcount() ?? count_letters($message);
 433          } else {
 434              $wordcount = null;
 435              $charcount = null;
 436          }
 437  
 438          return [
 439              'id' => $post->get_id(),
 440              'subject' => $subject,
 441              'replysubject' => $replysubject,
 442              'message' => $message,
 443              'messageformat' => $post->get_message_format(),
 444              'author' => $exportedauthor,
 445              'discussionid' => $post->get_discussion_id(),
 446              'hasparent' => $post->has_parent(),
 447              'parentid' => $post->has_parent() ? $post->get_parent_id() : null,
 448              'timecreated' => $timecreated,
 449              'timemodified' => $post->get_time_modified(),
 450              'unread' => ($loadcontent && $readreceiptcollection) ? !$readreceiptcollection->has_user_read_post($user, $post) : null,
 451              'isdeleted' => $isdeleted,
 452              'isprivatereply' => $isprivatereply,
 453              'haswordcount' => $showwordcount,
 454              'wordcount' => $wordcount,
 455              'charcount' => $charcount,
 456              'capabilities' => [
 457                  'view' => $canview,
 458                  'edit' => $canedit,
 459                  'delete' => $candelete,
 460                  'split' => $cansplit,
 461                  'reply' => $canreply,
 462                  'export' => $canexport,
 463                  'controlreadstatus' => $cancontrolreadstatus,
 464                  'canreplyprivately' => $canreplyprivately,
 465                  'selfenrol' => $canselfenrol
 466              ],
 467              'urls' => [
 468                  'view' => $viewurl ? $viewurl->out(false) : null,
 469                  'viewisolated' => $viewisolatedurl ? $viewisolatedurl->out(false) : null,
 470                  'viewparent' => $viewparenturl ? $viewparenturl->out(false) : null,
 471                  'edit' => $editurl ? $editurl->out(false) : null,
 472                  'delete' => $deleteurl ? $deleteurl->out(false) : null,
 473                  'split' => $spliturl ? $spliturl->out(false) : null,
 474                  'reply' => $replyurl ? $replyurl->out(false) : null,
 475                  'export' => $exporturl && $exporturl ? $exporturl->out(false) : null,
 476                  'markasread' => $markasreadurl ? $markasreadurl->out(false) : null,
 477                  'markasunread' => $markasunreadurl ? $markasunreadurl->out(false) : null,
 478                  'discuss' => $discussurl ? $discussurl->out(false) : null,
 479              ],
 480              'attachments' => ($exportattachments) ? $this->export_attachments($attachments, $post, $output, $canexport) : [],
 481              'tags' => ($loadcontent && $hastags) ? $this->export_tags($tags) : [],
 482              'html' => $includehtml ? [
 483                  'rating' => ($loadcontent && $hasrating) ? $output->render($rating) : null,
 484                  'taglist' => ($loadcontent && $hastags) ? $output->tag_list($tags) : null,
 485                  'authorsubheading' => ($loadcontent) ? $this->get_author_subheading_html($exportedauthor, $timecreated) : null
 486              ] : null
 487          ];
 488      }
 489  
 490      /**
 491       * Returns a list of objects that are related.
 492       *
 493       * @return array
 494       */
 495      protected static function define_related() {
 496          return [
 497              'capabilitymanager' => 'mod_forum\local\managers\capability',
 498              'readreceiptcollection' => 'mod_forum\local\entities\post_read_receipt_collection?',
 499              'urlfactory' => 'mod_forum\local\factories\url',
 500              'forum' => 'mod_forum\local\entities\forum',
 501              'discussion' => 'mod_forum\local\entities\discussion',
 502              'author' => 'mod_forum\local\entities\author',
 503              'authorcontextid' => 'int?',
 504              'user' => 'stdClass',
 505              'context' => 'context',
 506              'authorgroups' => 'stdClass[]',
 507              'attachments' => '\stored_file[]?',
 508              'tags' => '\core_tag_tag[]?',
 509              'rating' => 'rating?',
 510              'includehtml' => 'bool'
 511          ];
 512      }
 513  
 514      /**
 515       * This method returns the parameters for the post's message to
 516       * use with the function external_format_text().
 517       *
 518       * @return array
 519       */
 520      protected function get_format_parameters_for_message() {
 521          return [
 522              'component' => 'mod_forum',
 523              'filearea' => 'post',
 524              'itemid' => $this->post->get_id(),
 525              'options' => [
 526                  'para' => false,
 527                  'trusted' => $this->post->is_message_trusted()
 528              ]
 529          ];
 530      }
 531  
 532      /**
 533       * Get the message text from a post.
 534       *
 535       * @param post_entity $post The post
 536       * @return string
 537       */
 538      private function get_message(post_entity $post) : string {
 539          global $CFG;
 540  
 541          $message = $post->get_message();
 542  
 543          if (!empty($CFG->enableplagiarism)) {
 544              require_once($CFG->libdir . '/plagiarismlib.php');
 545              $forum = $this->related['forum'];
 546              $message .= plagiarism_get_links([
 547                  'userid' => $post->get_author_id(),
 548                  'content' => $message,
 549                  'cmid' => $forum->get_course_module_record()->id,
 550                  'course' => $forum->get_course_id(),
 551                  'forum' => $forum->get_id()
 552              ]);
 553          }
 554  
 555          return $message;
 556      }
 557  
 558      /**
 559       * Get the exported attachments for a post.
 560       *
 561       * @param stored_file[] $attachments The list of attachments for the post
 562       * @param post_entity $post The post being exported
 563       * @param renderer_base $output Renderer base
 564       * @param bool $canexport If the user can export the post (relates to portfolios not exporters like this class)
 565       * @return array
 566       */
 567      private function export_attachments(array $attachments, post_entity $post, renderer_base $output, bool $canexport) : array {
 568          global $CFG;
 569  
 570          $urlfactory = $this->related['urlfactory'];
 571          $enableplagiarism = $CFG->enableplagiarism;
 572          $forum = $this->related['forum'];
 573          $context = $this->related['context'];
 574  
 575          if ($enableplagiarism) {
 576              require_once($CFG->libdir . '/plagiarismlib.php' );
 577          }
 578  
 579          return array_map(function($attachment) use (
 580              $output,
 581              $enableplagiarism,
 582              $canexport,
 583              $context,
 584              $forum,
 585              $post,
 586              $urlfactory
 587          ) {
 588              $exporter = new stored_file_exporter($attachment, ['context' => $context]);
 589              $exportedattachment = $exporter->export($output);
 590              $exporturl = $canexport ? $urlfactory->get_export_attachment_url_from_post_and_attachment($post, $attachment) : null;
 591  
 592              if ($enableplagiarism) {
 593                  $plagiarismhtml = plagiarism_get_links([
 594                      'userid' => $post->get_author_id(),
 595                      'file' => $attachment,
 596                      'cmid' => $forum->get_course_module_record()->id,
 597                      'course' => $forum->get_course_id(),
 598                      'forum' => $forum->get_id()
 599                  ]);
 600              } else {
 601                  $plagiarismhtml = null;
 602              }
 603  
 604              $exportedattachment->urls = [
 605                  'export' => $exporturl ? $exporturl->out(false) : null
 606              ];
 607              $exportedattachment->html = [
 608                  'plagiarism' => $plagiarismhtml
 609              ];
 610  
 611              return $exportedattachment;
 612          }, $attachments);
 613      }
 614  
 615      /**
 616       * Export the list of tags.
 617       *
 618       * @param core_tag_tag[] $tags List of tags to export
 619       * @return array
 620       */
 621      private function export_tags(array $tags) : array {
 622          $user = $this->related['user'];
 623          $context = $this->related['context'];
 624          $capabilitymanager = $this->related['capabilitymanager'];
 625          $canmanagetags = $capabilitymanager->can_manage_tags($user);
 626  
 627          return array_values(array_map(function($tag) use ($context, $canmanagetags) {
 628              $viewurl = core_tag_tag::make_url($tag->tagcollid, $tag->rawname, 0, $context->id);
 629              return [
 630                  'id' => $tag->taginstanceid,
 631                  'tagid' => $tag->id,
 632                  'isstandard' => $tag->isstandard,
 633                  'displayname' => $tag->get_display_name(),
 634                  'flag' => $canmanagetags && !empty($tag->flag),
 635                  'urls' => [
 636                      'view' => $viewurl->out(false)
 637                  ]
 638              ];
 639          }, $tags));
 640      }
 641  
 642      /**
 643       * Get the HTML to display as a subheading in a post.
 644       *
 645       * @param stdClass $exportedauthor The exported author object
 646       * @param int $timecreated The post time created timestamp if it's to be displayed
 647       * @return string
 648       */
 649      private function get_author_subheading_html(stdClass $exportedauthor, int $timecreated) : string {
 650          $fullname = $exportedauthor->fullname;
 651          $profileurl = $exportedauthor->urls['profile'] ?? null;
 652          $name = $profileurl ? "<a href=\"{$profileurl}\">{$fullname}</a>" : $fullname;
 653          $date = userdate_htmltime($timecreated, get_string('strftimedaydatetime', 'core_langconfig'));
 654          return get_string('bynameondate', 'mod_forum', ['name' => $name, 'date' => $date]);
 655      }
 656  
 657      /**
 658       * Get the start time for a post.
 659       *
 660       * @param discussion_entity $discussion entity
 661       * @param post_entity $post entity
 662       * @return int The start time (timestamp) for a post
 663       */
 664      private function get_start_time(discussion_entity $discussion, post_entity $post) {
 665          global $CFG;
 666  
 667          $posttime = $post->get_time_created();
 668          $discussiontime = $discussion->get_time_start();
 669          if (!empty($CFG->forum_enabletimedposts) && ($discussiontime > $posttime)) {
 670              return $discussiontime;
 671          }
 672          return $posttime;
 673      }
 674  }