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.
   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   * Base class for dataformat.
  19   *
  20   * @package    core
  21   * @subpackage dataformat
  22   * @copyright  2016 Brendan Heywood (brendan@catalyst-au.net)
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  namespace core\dataformat;
  27  
  28  use coding_exception;
  29  
  30  /**
  31   * Base class for dataformat.
  32   *
  33   * @package    core
  34   * @subpackage dataformat
  35   * @copyright  2016 Brendan Heywood (brendan@catalyst-au.net)
  36   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  37   */
  38  abstract class base {
  39  
  40      /** @var $mimetype */
  41      protected $mimetype = "text/plain";
  42  
  43      /** @var $extension */
  44      protected $extension = ".txt";
  45  
  46      /** @var $filename */
  47      protected $filename = '';
  48  
  49      /** @var string The location to store the output content */
  50      protected $filepath = '';
  51  
  52      /**
  53       * Get the file extension
  54       *
  55       * @return string file extension
  56       */
  57      public function get_extension() {
  58          return $this->extension;
  59      }
  60  
  61      /**
  62       * Set download filename base
  63       *
  64       * @param string $filename
  65       */
  66      public function set_filename($filename) {
  67          $this->filename = $filename;
  68      }
  69  
  70      /**
  71       * Set file path when writing to file
  72       *
  73       * @param string $filepath
  74       * @throws coding_exception
  75       */
  76      public function set_filepath(string $filepath): void {
  77          $filedir = dirname($filepath);
  78          if (!is_writable($filedir)) {
  79              throw new coding_exception('File path is not writable');
  80          }
  81  
  82          $this->filepath = $filepath;
  83  
  84          // Some dataformat writers may expect filename to be set too.
  85          $this->set_filename(pathinfo($this->filepath, PATHINFO_FILENAME));
  86      }
  87  
  88      /**
  89       * Set the title of the worksheet inside a spreadsheet
  90       *
  91       * For some formats this will be ignored.
  92       *
  93       * @param string $title
  94       */
  95      public function set_sheettitle($title) {
  96      }
  97  
  98      /**
  99       * Output file headers to initialise the download of the file.
 100       */
 101      public function send_http_headers() {
 102          if (defined('BEHAT_SITE_RUNNING') || PHPUNIT_TEST) {
 103              // For text based formats - we cannot test the output with behat if we force a file download.
 104              return;
 105          }
 106          if (is_https()) {
 107              // HTTPS sites - watch out for IE! KB812935 and KB316431.
 108              header('Cache-Control: max-age=10');
 109              header('Pragma: ');
 110          } else {
 111              // Normal http - prevent caching at all cost.
 112              header('Cache-Control: private, must-revalidate, pre-check=0, post-check=0, max-age=0');
 113              header('Pragma: no-cache');
 114          }
 115          header('Expires: '. gmdate('D, d M Y H:i:s', 0) .' GMT');
 116          header("Content-Type: $this->mimetype\n");
 117          $filename = $this->filename . $this->get_extension();
 118          header("Content-Disposition: attachment; filename=\"$filename\"");
 119      }
 120  
 121      /**
 122       * Set the dataformat to be output to current file. Calling code must call {@see base::close_output_to_file()} when finished
 123       */
 124      public function start_output_to_file(): void {
 125          // Raise memory limit to ensure we can store the entire content. Start collecting output.
 126          raise_memory_limit(MEMORY_EXTRA);
 127  
 128          ob_start();
 129          $this->start_output();
 130      }
 131  
 132      /**
 133       * Write the start of the file.
 134       */
 135      public function start_output() {
 136          // Override me if needed.
 137      }
 138  
 139      /**
 140       * Write the start of the sheet we will be adding data to.
 141       *
 142       * @param array $columns
 143       */
 144      public function start_sheet($columns) {
 145          // Override me if needed.
 146      }
 147  
 148      /**
 149       * Method to define whether the dataformat supports export of HTML
 150       *
 151       * @return bool
 152       */
 153      public function supports_html(): bool {
 154          return false;
 155      }
 156  
 157      /**
 158       * Apply formatting to the cells of a given record
 159       *
 160       * @param array|\stdClass $record
 161       * @return array
 162       */
 163      protected function format_record($record): array {
 164          $record = (array)$record;
 165  
 166          // If the dataformat supports export of HTML, we need to allow them to manage embedded images.
 167          if ($this->supports_html()) {
 168              $record = array_map([$this, 'replace_pluginfile_images'], $record);
 169          }
 170  
 171          return $record;
 172      }
 173  
 174      /**
 175       * Given a stored_file, return a suitable source attribute for an img element in the export (or null to use the original)
 176       *
 177       * @param \stored_file $file
 178       * @return string|null
 179       */
 180      protected function export_html_image_source(\stored_file $file): ?string {
 181          return null;
 182      }
 183  
 184      /**
 185       * We need to locate all img tags within a given cell that match pluginfile URL's. Partly so the exported file will show
 186       * the image without requiring the user is logged in; and also to prevent some of the dataformats requesting the file
 187       * themselves, which is likely to fail due to them not having an active session
 188       *
 189       * @param string|null $content
 190       * @return string
 191       */
 192      protected function replace_pluginfile_images(?string $content): string {
 193          $content = (string)$content;
 194  
 195          // Examine content to see if it contains any HTML image tags.
 196          return preg_replace_callback('/(?<pre><img[^>]+src=")(?<source>[^"]*)(?<post>".*>)/i', function(array $matches) {
 197              $source = $matches['source'];
 198  
 199              // Now check if the image source looks like a pluginfile URL.
 200              if (preg_match('/pluginfile.php\/(?<context>\d+)\/(?<component>[^\/]+)\/(?<filearea>[^\/]+)\/(?:(?<itemid>\d+)\/)?' .
 201                      '(?<path>.*)/u', $source, $args)) {
 202  
 203                  $context = $args['context'];
 204                  $component = clean_param($args['component'], PARAM_COMPONENT);
 205                  $filearea = clean_param($args['filearea'], PARAM_AREA);
 206                  $itemid = $args['itemid'] ?: 0;
 207                  $path = clean_param(urldecode($args['path']), PARAM_PATH);
 208  
 209                  // Try and get the matching file from storage, allow the dataformat to define the replacement source.
 210                  $fullpath = "/{$context}/{$component}/{$filearea}/{$itemid}/{$path}";
 211                  if ($file = get_file_storage()->get_file_by_hash(sha1($fullpath))) {
 212                      $exportsource = $this->export_html_image_source($file);
 213  
 214                      if ($exportsource) {
 215                          $source = $exportsource;
 216                      }
 217                  }
 218              }
 219  
 220              return $matches['pre'] . $source . $matches['post'];
 221          }, $content);
 222      }
 223  
 224      /**
 225       * Write a single record
 226       *
 227       * @param array $record
 228       * @param int $rownum
 229       */
 230      abstract public function write_record($record, $rownum);
 231  
 232      /**
 233       * Write the end of the sheet containing the data.
 234       *
 235       * @param array $columns
 236       */
 237      public function close_sheet($columns) {
 238          // Override me if needed.
 239      }
 240  
 241      /**
 242       * Write the end of the file.
 243       */
 244      public function close_output() {
 245          // Override me if needed.
 246      }
 247  
 248      /**
 249       * Write the data to disk. Calling code should have previously called {@see base::start_output_to_file()}
 250       *
 251       * @return bool Whether the write succeeded
 252       */
 253      public function close_output_to_file(): bool {
 254          $this->close_output();
 255  
 256          $filecontent = ob_get_contents();
 257          ob_end_clean();
 258  
 259          return file_put_contents($this->filepath, $filecontent) !== false;
 260      }
 261  }