Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [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                    JOIN {files} conversionsourcefile ON conversionsourcefile.id = c.sourcefileid
 134               LEFT JOIN {files} conversiondestfile ON conversiondestfile.id = c.destfileid
 135                   WHERE conversionsourcefile.contenthash = :ccontenthash
 136                         AND c.targetformat = :cformat
 137                         AND (c.destfileid IS NULL OR conversiondestfile.id IS NOT NULL)";
 138  
 139          // Fetch a empty conversion record for each source/destination combination that we find to match where the
 140          // destination file is in the correct filearea/filepath/filename combination to meet the requirements.
 141          // This ensures that existing conversions are used where possible, even if there is no 'conversion' record for
 142          // them.
 143          $sql .= "
 144              UNION ALL
 145                  SELECT
 146                      NULL AS conversionid,
 147                      orphanedsourcefile.id AS conversionsourcefileid,
 148                      :oformat AS conversiontargetformat,
 149                      2 AS conversionstatus,
 150                      NULL AS conversionstatusmessage,
 151                      NULL AS conversionconverter,
 152                      orphaneddestfile.id AS conversiondestfileid,
 153                      NULL AS conversiondata,
 154                      0 AS conversiontimecreated,
 155                      0 AS conversiontimemodified,
 156                      0 AS conversionusermodified
 157                  FROM {files} orphanedsourcefile
 158                  INNER JOIN {files} orphaneddestfile ON (
 159                          orphaneddestfile.filename = orphanedsourcefile.contenthash
 160                      AND orphaneddestfile.component = 'core'
 161                      AND orphaneddestfile.filearea = 'documentconversion'
 162                      AND orphaneddestfile.filepath = :ofilepath
 163                  )
 164                  LEFT JOIN {" . self::TABLE . "} orphanedconversion ON orphanedconversion.destfileid = orphaneddestfile.id
 165                  WHERE
 166                      orphanedconversion.id IS NULL
 167                  AND
 168                      orphanedsourcefile.id = :osourcefileid
 169                  ";
 170          $records = $DB->get_records_sql($sql, [
 171              'ccontenthash' => $file->get_contenthash(),
 172              'osourcefileid' => $file->get_id(),
 173              'ofilepath' => "/{$format}/",
 174              'cformat' => $format,
 175              'oformat' => $format,
 176          ]);
 177  
 178          foreach ($records as $record) {
 179              $data = self::extract_record($record, 'conversion');
 180              $newrecord = new static(0, $data);
 181              $instances[] = $newrecord;
 182          }
 183  
 184          return $instances;
 185      }
 186  
 187      /**
 188       * Remove all old conversion records.
 189       */
 190      public static function remove_old_conversion_records() {
 191          global $DB;
 192  
 193          $DB->delete_records_select(self::TABLE, 'timemodified <= :weekagosecs', [
 194              'weekagosecs' => time() - WEEKSECS,
 195          ]);
 196      }
 197  
 198      /**
 199       * Remove orphan records.
 200       *
 201       * Records are considered orphans when their source file not longer exists.
 202       * In this scenario we do not want to keep the converted file any longer,
 203       * in particular to be compliant with privacy laws.
 204       */
 205      public static function remove_orphan_records() {
 206          global $DB;
 207  
 208          $sql = "
 209              SELECT c.id
 210                FROM {" . self::TABLE . "} c
 211           LEFT JOIN {files} f
 212                  ON f.id = c.sourcefileid
 213               WHERE f.id IS NULL";
 214          $ids = $DB->get_fieldset_sql($sql, []);
 215  
 216          if (empty($ids)) {
 217              return;
 218          }
 219  
 220          list($insql, $inparams) = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED);
 221          $DB->delete_records_select(self::TABLE, "id $insql", $inparams);
 222      }
 223  
 224      /**
 225       * Set the source file id for the conversion.
 226       *
 227       * @param   stored_file $file The file to convert
 228       * @return  $this
 229       */
 230      public function set_sourcefile(stored_file $file) {
 231          $this->raw_set('sourcefileid', $file->get_id());
 232  
 233          return $this;
 234      }
 235  
 236      /**
 237       * Fetch the source file.
 238       *
 239       * @return  stored_file|false The source file
 240       */
 241      public function get_sourcefile() {
 242          $fs = get_file_storage();
 243  
 244          return $fs->get_file_by_id($this->get('sourcefileid'));
 245      }
 246  
 247      /**
 248       * Set the destination file for this conversion.
 249       *
 250       * @param   string $filepath The path to the converted file
 251       * @return  $this
 252       */
 253      public function store_destfile_from_path($filepath) {
 254          if ($record = $this->get_file_record()) {
 255              $fs = get_file_storage();
 256              $existing = $fs->get_file(
 257                  $record['contextid'],
 258                  $record['component'],
 259                  $record['filearea'],
 260                  $record['itemid'],
 261                  $record['filepath'],
 262                  $record['filename']
 263              );
 264              if ($existing) {
 265                  $existing->delete();
 266              }
 267              $file = $fs->create_file_from_pathname($record, $filepath);
 268  
 269              $this->raw_set('destfileid', $file->get_id());
 270          }
 271  
 272          return $this;
 273      }
 274  
 275      /**
 276       * Set the destination file for this conversion.
 277       *
 278       * @param   string $content The content of the converted file
 279       * @return  $this
 280       */
 281      public function store_destfile_from_string($content) {
 282          if ($record = $this->get_file_record()) {
 283              $fs = get_file_storage();
 284              $existing = $fs->get_file(
 285                  $record['contextid'],
 286                  $record['component'],
 287                  $record['filearea'],
 288                  $record['itemid'],
 289                  $record['filepath'],
 290                  $record['filename']
 291              );
 292              if ($existing) {
 293                  $existing->delete();
 294              }
 295              $file = $fs->create_file_from_string($record, $content);
 296  
 297              $this->raw_set('destfileid', $file->get_id());
 298          }
 299  
 300          return $this;
 301      }
 302  
 303      /**
 304       * Get the destination file.
 305       *
 306       * @return  stored_file|bool Destination file
 307       */
 308      public function get_destfile() {
 309          $fs = get_file_storage();
 310  
 311          return $fs->get_file_by_id($this->get('destfileid'));
 312      }
 313  
 314      /**
 315       * Helper to ensure that the returned status is always an int.
 316       *
 317       * @return  int status
 318       */
 319      protected function get_status() {
 320          return (int) $this->raw_get('status');
 321      }
 322  
 323      /**
 324       * Get an instance of the current converter.
 325       *
 326       * @return  converter_interface|false current converter instance
 327       */
 328      public function get_converter_instance() {
 329          $currentconverter = $this->get('converter');
 330  
 331          if ($currentconverter && class_exists($currentconverter)) {
 332              return new $currentconverter();
 333          } else {
 334              return false;
 335          }
 336      }
 337  
 338      /**
 339       * Transform data into a storable format.
 340       *
 341       * @param   \stdClass $data The data to be stored
 342       * @return  $this
 343       */
 344      protected function set_data($data) {
 345          $this->raw_set('data', json_encode($data));
 346  
 347          return $this;
 348      }
 349  
 350      /**
 351       * Transform data into a storable format.
 352       *
 353       * @return  \stdClass The stored data
 354       */
 355      protected function get_data() {
 356          $data = $this->raw_get('data');
 357  
 358          if (!empty($data)) {
 359              return json_decode($data);
 360          }
 361  
 362          return (object) [];
 363      }
 364  
 365      /**
 366       * Return the file record base for use in the files table.
 367       *
 368       * @return  array|bool
 369       */
 370      protected function get_file_record() {
 371          $file = $this->get_sourcefile();
 372  
 373          if (!$file) {
 374              // If the source file was removed before we completed, we must return early.
 375              return false;
 376          }
 377  
 378          return [
 379              'contextid' => \context_system::instance()->id,
 380              'component' => 'core',
 381              'filearea'  => 'documentconversion',
 382              'itemid'    => 0,
 383              'filepath'  => "/" . $this->get('targetformat') . "/",
 384              'filename'  => $file->get_contenthash(),
 385          ];
 386      }
 387  }