Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

Differences Between: [Versions 400 and 401] [Versions 400 and 402] [Versions 400 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   * Classes for converting files between different file formats.
  19   *
  20   * @package    core_files
  21   * @copyright  2017 Andrew Nicols <andrew@nicols.co.uk>
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  namespace core_files;
  25  
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  use stored_file;
  29  
  30  /**
  31   * Class representing a conversion currently in progress.
  32   *
  33   * @package    core_files
  34   * @copyright  2017 Andrew Nicols <andrew@nicols.co.uk>
  35   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  36   */
  37  class conversion extends \core\persistent {
  38  
  39      /**
  40       * Status value representing a conversion waiting to start.
  41       */
  42      const STATUS_PENDING = 0;
  43  
  44      /**
  45       * Status value representing a conversion in progress.
  46       */
  47      const STATUS_IN_PROGRESS = 1;
  48  
  49      /**
  50       * Status value representing a successful conversion.
  51       */
  52      const STATUS_COMPLETE = 2;
  53  
  54      /**
  55       * Status value representing a failed conversion.
  56       */
  57      const STATUS_FAILED = -1;
  58  
  59      /**
  60       * Table name for this persistent.
  61       */
  62      const TABLE = 'file_conversion';
  63  
  64      /**
  65       * Define properties.
  66       *
  67       * @return array
  68       */
  69      protected static function define_properties() {
  70          return array(
  71              'sourcefileid' => [
  72                  'type' => PARAM_INT,
  73              ],
  74              'targetformat' => [
  75                  'type' => PARAM_ALPHANUMEXT,
  76              ],
  77              'status' => [
  78                  'type' => PARAM_INT,
  79                  'choices' => [
  80                      self::STATUS_PENDING,
  81                      self::STATUS_IN_PROGRESS,
  82                      self::STATUS_COMPLETE,
  83                      self::STATUS_FAILED,
  84                  ],
  85                  'default' => self::STATUS_PENDING,
  86              ],
  87              'statusmessage' => [
  88                  'type' => PARAM_RAW,
  89                  'null' => NULL_ALLOWED,
  90                  'default' => null,
  91              ],
  92              'converter' => [
  93                  'type' => PARAM_RAW,
  94                  'null' => NULL_ALLOWED,
  95                  'default' => null,
  96              ],
  97              'destfileid' => [
  98                  'type' => PARAM_INT,
  99                  'null' => NULL_ALLOWED,
 100                  'default' => null,
 101              ],
 102              'data' => [
 103                  'type' => PARAM_RAW,
 104                  'null' => NULL_ALLOWED,
 105                  'default' => null,
 106              ],
 107          );
 108      }
 109  
 110      /**
 111       * Fetch all conversions relating to the specified file.
 112       *
 113       * Only conversions which have a valid file are returned.
 114       *
 115       * @param   stored_file $file The source file being converted
 116       * @param   string $format The targetforamt to filter to
 117       * @return  conversion[]
 118       */
 119      public static function get_conversions_for_file(stored_file $file, $format) {
 120          global $DB;
 121          $instances = [];
 122  
 123          // Conversion records are intended for tracking a conversion in progress or recently completed.
 124          // The record is removed periodically, but the destination file is not.
 125          // We need to fetch all conversion records which match the source file and target, and also all source and
 126          // destination files which do not have a conversion record.
 127          $sqlfields = self::get_sql_fields('c', 'conversion');
 128  
 129          // Fetch actual conversions which relate to the specified source file, and have a matching conversion record,
 130          // and either have a valid destination file which still exists, or do not have a destination file at all.
 131          $sql = "SELECT {$sqlfields}
 132                  FROM {" . self::TABLE . "} c
 133                  INNER JOIN {files} conversionsourcefile ON conversionsourcefile.id = c.sourcefileid
 134                  LEFT JOIN {files} conversiondestfile ON conversiondestfile.id = c.destfileid
 135                  WHERE
 136                      conversionsourcefile.contenthash = :ccontenthash
 137                  AND c.targetformat = :cformat
 138                  AND (
 139                      c.destfileid IS NULL OR conversiondestfile.id IS NOT NULL
 140                  )";
 141  
 142          // Fetch a empty conversion record for each source/destination combination that we find to match where the
 143          // destination file is in the correct filearea/filepath/filename combination to meet the requirements.
 144          // This ensures that existing conversions are used where possible, even if there is no 'conversion' record for
 145          // them.
 146          $sql .= "
 147              UNION ALL
 148                  SELECT
 149                      NULL AS conversionid,
 150                      orphanedsourcefile.id AS conversionsourcefileid,
 151                      :oformat AS conversiontargetformat,
 152                      2 AS conversionstatus,
 153                      NULL AS conversionstatusmessage,
 154                      NULL AS conversionconverter,
 155                      orphaneddestfile.id AS conversiondestfileid,
 156                      NULL AS conversiondata,
 157                      0 AS conversiontimecreated,
 158                      0 AS conversiontimemodified,
 159                      0 AS conversionusermodified
 160                  FROM {files} orphanedsourcefile
 161                  INNER JOIN {files} orphaneddestfile ON (
 162                          orphaneddestfile.filename = orphanedsourcefile.contenthash
 163                      AND orphaneddestfile.component = 'core'
 164                      AND orphaneddestfile.filearea = 'documentconversion'
 165                      AND orphaneddestfile.filepath = :ofilepath
 166                  )
 167                  LEFT JOIN {" . self::TABLE . "} orphanedconversion ON orphanedconversion.destfileid = orphaneddestfile.id
 168                  WHERE
 169                      orphanedconversion.id IS NULL
 170                  AND
 171                      orphanedsourcefile.id = :osourcefileid
 172                  ";
 173          $records = $DB->get_records_sql($sql, [
 174              'ccontenthash' => $file->get_contenthash(),
 175              'osourcefileid' => $file->get_id(),
 176              'cfilepath' => "/{$format}/",
 177              'ofilepath' => "/{$format}/",
 178              'cformat' => $format,
 179              'oformat' => $format,
 180          ]);
 181  
 182          foreach ($records as $record) {
 183              $data = self::extract_record($record, 'conversion');
 184              $newrecord = new static(0, $data);
 185              $instances[] = $newrecord;
 186          }
 187  
 188          return $instances;
 189      }
 190  
 191      /**
 192       * Remove all old conversion records.
 193       */
 194      public static function remove_old_conversion_records() {
 195          global $DB;
 196  
 197          $DB->delete_records_select(self::TABLE, 'timemodified <= :weekagosecs', [
 198              'weekagosecs' => time() - WEEKSECS,
 199          ]);
 200      }
 201  
 202      /**
 203       * Remove orphan records.
 204       *
 205       * Records are considered orphans when their source file not longer exists.
 206       * In this scenario we do not want to keep the converted file any longer,
 207       * in particular to be compliant with privacy laws.
 208       */
 209      public static function remove_orphan_records() {
 210          global $DB;
 211  
 212          $sql = "
 213              SELECT c.id
 214                FROM {" . self::TABLE . "} c
 215           LEFT JOIN {files} f
 216                  ON f.id = c.sourcefileid
 217               WHERE f.id IS NULL";
 218          $ids = $DB->get_fieldset_sql($sql, []);
 219  
 220          if (empty($ids)) {
 221              return;
 222          }
 223  
 224          list($insql, $inparams) = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED);
 225          $DB->delete_records_select(self::TABLE, "id $insql", $inparams);
 226      }
 227  
 228      /**
 229       * Set the source file id for the conversion.
 230       *
 231       * @param   stored_file $file The file to convert
 232       * @return  $this
 233       */
 234      public function set_sourcefile(stored_file $file) {
 235          $this->raw_set('sourcefileid', $file->get_id());
 236  
 237          return $this;
 238      }
 239  
 240      /**
 241       * Fetch the source file.
 242       *
 243       * @return  stored_file|false The source file
 244       */
 245      public function get_sourcefile() {
 246          $fs = get_file_storage();
 247  
 248          return $fs->get_file_by_id($this->get('sourcefileid'));
 249      }
 250  
 251      /**
 252       * Set the destination file for this conversion.
 253       *
 254       * @param   string $filepath The path to the converted file
 255       * @return  $this
 256       */
 257      public function store_destfile_from_path($filepath) {
 258          if ($record = $this->get_file_record()) {
 259              $fs = get_file_storage();
 260              $existing = $fs->get_file(
 261                  $record['contextid'],
 262                  $record['component'],
 263                  $record['filearea'],
 264                  $record['itemid'],
 265                  $record['filepath'],
 266                  $record['filename']
 267              );
 268              if ($existing) {
 269                  $existing->delete();
 270              }
 271              $file = $fs->create_file_from_pathname($record, $filepath);
 272  
 273              $this->raw_set('destfileid', $file->get_id());
 274          }
 275  
 276          return $this;
 277      }
 278  
 279      /**
 280       * Set the destination file for this conversion.
 281       *
 282       * @param   string $content The content of the converted file
 283       * @return  $this
 284       */
 285      public function store_destfile_from_string($content) {
 286          if ($record = $this->get_file_record()) {
 287              $fs = get_file_storage();
 288              $existing = $fs->get_file(
 289                  $record['contextid'],
 290                  $record['component'],
 291                  $record['filearea'],
 292                  $record['itemid'],
 293                  $record['filepath'],
 294                  $record['filename']
 295              );
 296              if ($existing) {
 297                  $existing->delete();
 298              }
 299              $file = $fs->create_file_from_string($record, $content);
 300  
 301              $this->raw_set('destfileid', $file->get_id());
 302          }
 303  
 304          return $this;
 305      }
 306  
 307      /**
 308       * Get the destination file.
 309       *
 310       * @return  stored_file|bool Destination file
 311       */
 312      public function get_destfile() {
 313          $fs = get_file_storage();
 314  
 315          return $fs->get_file_by_id($this->get('destfileid'));
 316      }
 317  
 318      /**
 319       * Helper to ensure that the returned status is always an int.
 320       *
 321       * @return  int status
 322       */
 323      protected function get_status() {
 324          return (int) $this->raw_get('status');
 325      }
 326  
 327      /**
 328       * Get an instance of the current converter.
 329       *
 330       * @return  converter_interface|false current converter instance
 331       */
 332      public function get_converter_instance() {
 333          $currentconverter = $this->get('converter');
 334  
 335          if ($currentconverter && class_exists($currentconverter)) {
 336              return new $currentconverter();
 337          } else {
 338              return false;
 339          }
 340      }
 341  
 342      /**
 343       * Transform data into a storable format.
 344       *
 345       * @param   \stdClass $data The data to be stored
 346       * @return  $this
 347       */
 348      protected function set_data($data) {
 349          $this->raw_set('data', json_encode($data));
 350  
 351          return $this;
 352      }
 353  
 354      /**
 355       * Transform data into a storable format.
 356       *
 357       * @return  \stdClass The stored data
 358       */
 359      protected function get_data() {
 360          $data = $this->raw_get('data');
 361  
 362          if (!empty($data)) {
 363              return json_decode($data);
 364          }
 365  
 366          return (object) [];
 367      }
 368  
 369      /**
 370       * Return the file record base for use in the files table.
 371       *
 372       * @return  array|bool
 373       */
 374      protected function get_file_record() {
 375          $file = $this->get_sourcefile();
 376  
 377          if (!$file) {
 378              // If the source file was removed before we completed, we must return early.
 379              return false;
 380          }
 381  
 382          return [
 383              'contextid' => \context_system::instance()->id,
 384              'component' => 'core',
 385              'filearea'  => 'documentconversion',
 386              'itemid'    => 0,
 387              'filepath'  => "/" . $this->get('targetformat') . "/",
 388              'filename'  => $file->get_contenthash(),
 389          ];
 390      }
 391  }