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 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   * This file contains the combined document class for the assignfeedback_editpdf plugin.
  19   *
  20   * @package   assignfeedback_editpdf
  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  
  25  namespace assignfeedback_editpdf;
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  /**
  30   * The combined_document class for the assignfeedback_editpdf plugin.
  31   *
  32   * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
  33   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  34   */
  35  class combined_document {
  36  
  37      /**
  38       * Status value representing a conversion waiting to start.
  39       */
  40      const STATUS_PENDING_INPUT = 0;
  41  
  42      /**
  43       * Status value representing all documents ready to be combined.
  44       */
  45      const STATUS_READY = 1;
  46  
  47      /**
  48       * Status value representing all documents are ready to be combined as are supported.
  49       */
  50      const STATUS_READY_PARTIAL = 3;
  51  
  52      /**
  53       * Status value representing a successful conversion.
  54       */
  55      const STATUS_COMPLETE = 2;
  56  
  57      /**
  58       * Status value representing a permanent error.
  59       */
  60      const STATUS_FAILED = -1;
  61  
  62      /**
  63       * The list of files which make this document.
  64       */
  65      protected $sourcefiles = [];
  66  
  67      /**
  68       * The resultant combined file.
  69       */
  70      protected $combinedfile;
  71  
  72      /**
  73       * The combination status.
  74       */
  75      protected $combinationstatus = null;
  76  
  77      /**
  78       * The number of pages in the combined PDF.
  79       */
  80      protected $pagecount = 0;
  81  
  82      /**
  83       * Check the current status of the document combination.
  84       * Note that the combined document may not contain all the source files if some of the
  85       * source files were not able to be converted. An example is an audio file with a pdf cover sheet. Only
  86       * the cover sheet will be included in the combined document.
  87       *
  88       * @return  int
  89       */
  90      public function get_status() {
  91          if ($this->combinedfile) {
  92              // The combined file exists. Report success.
  93              return self::STATUS_COMPLETE;
  94          }
  95  
  96          if (empty($this->sourcefiles)) {
  97              // There are no source files to combine.
  98              return self::STATUS_FAILED;
  99          }
 100  
 101          if (!empty($this->combinationstatus)) {
 102              // The combination is in progress and has set a status.
 103              // Return it instead.
 104              return $this->combinationstatus;
 105          }
 106  
 107          $pending = false;
 108          $partial = false;
 109          foreach ($this->sourcefiles as $file) {
 110              // The combined file has not yet been generated.
 111              // Check the status of each source file.
 112              if (is_a($file, \core_files\conversion::class)) {
 113                  $status = $file->get('status');
 114                  switch ($status) {
 115                      case \core_files\conversion::STATUS_IN_PROGRESS:
 116                      case \core_files\conversion::STATUS_PENDING:
 117                          $pending = true;
 118                          break;
 119  
 120                      // There are 4 status flags, so the only remaining one is complete which is fine.
 121                      case \core_files\conversion::STATUS_FAILED:
 122                          $partial = true;
 123                          break;
 124                  }
 125              }
 126          }
 127          if ($pending) {
 128              return self::STATUS_PENDING_INPUT;
 129          } else {
 130              if ($partial) {
 131                  return self::STATUS_READY_PARTIAL;
 132              }
 133              return self::STATUS_READY;
 134          }
 135      }
 136      /**
 137       * Set the completed combined file.
 138       *
 139       * @param   stored_file $file The completed document for all files to be combined.
 140       * @return  $this
 141       */
 142      public function set_combined_file($file) {
 143          $this->combinedfile = $file;
 144  
 145          return $this;
 146      }
 147  
 148      /**
 149       * Return true of the combined file contained only some of the submission files.
 150       *
 151       * @return  boolean
 152       */
 153      public function is_partial_conversion() {
 154          $combinedfile = $this->get_combined_file();
 155          if (empty($combinedfile)) {
 156              return false;
 157          }
 158          $filearea = $combinedfile->get_filearea();
 159          return $filearea == document_services::PARTIAL_PDF_FILEAREA;
 160      }
 161  
 162      /**
 163       * Retrieve the completed combined file.
 164       *
 165       * @return  stored_file
 166       */
 167      public function get_combined_file() {
 168          return $this->combinedfile;
 169      }
 170  
 171      /**
 172       * Set all source files which are to be combined.
 173       *
 174       * @param   stored_file|conversion[] $files The complete list of all source files to be combined.
 175       * @return  $this
 176       */
 177      public function set_source_files($files) {
 178          $this->sourcefiles = $files;
 179  
 180          return $this;
 181      }
 182  
 183      /**
 184       * Add an additional source file to the end of the existing list.
 185       *
 186       * @param   stored_file|conversion $file The file to add to the end of the list.
 187       * @return  $this
 188       */
 189      public function add_source_file($file) {
 190          $this->sourcefiles[] = $file;
 191  
 192          return $this;
 193      }
 194  
 195      /**
 196       * Retrieve the complete list of source files.
 197       *
 198       * @return  stored_file|conversion[]
 199       */
 200      public function get_source_files() {
 201          return $this->sourcefiles;
 202      }
 203  
 204      /**
 205       * Refresh the files.
 206       *
 207       * This includes polling any pending conversions to see if they are complete.
 208       *
 209       * @return  $this
 210       */
 211      public function refresh_files() {
 212          $converter = new \core_files\converter();
 213          foreach ($this->sourcefiles as $file) {
 214              if (is_a($file, \core_files\conversion::class)) {
 215                  $status = $file->get('status');
 216                  switch ($status) {
 217                      case \core_files\conversion::STATUS_COMPLETE:
 218                          continue 2;
 219                          break;
 220                      default:
 221                          $converter->poll_conversion($conversion);
 222                  }
 223              }
 224          }
 225  
 226          return $this;
 227      }
 228  
 229      /**
 230       * Combine all source files into a single PDF and store it in the
 231       * file_storage API using the supplied contextid and itemid.
 232       *
 233       * @param   int $contextid The contextid for the file to be stored under
 234       * @param   int $itemid The itemid for the file to be stored under
 235       * @return  $this
 236       */
 237      public function combine_files($contextid, $itemid) {
 238          global $CFG;
 239  
 240          $currentstatus = $this->get_status();
 241          $readystatuslist = [self::STATUS_READY, self::STATUS_READY_PARTIAL];
 242          if ($currentstatus === self::STATUS_FAILED) {
 243              $this->store_empty_document($contextid, $itemid);
 244  
 245              return $this;
 246          } else if (!in_array($currentstatus, $readystatuslist)) {
 247              // The document is either:
 248              // * already combined; or
 249              // * pending input being fully converted; or
 250              // * unable to continue due to an issue with the input documents.
 251              //
 252              // Exit early as we cannot continue.
 253              return $this;
 254          }
 255  
 256          require_once($CFG->libdir . '/pdflib.php');
 257  
 258          $pdf = new pdf();
 259          $files = $this->get_source_files();
 260          $compatiblepdfs = [];
 261  
 262          foreach ($files as $file) {
 263              // Check that each file is compatible and add it to the list.
 264              // Note: We drop non-compatible files.
 265              $compatiblepdf = false;
 266              if (is_a($file, \core_files\conversion::class)) {
 267                  $status = $file->get('status');
 268                  if ($status == \core_files\conversion::STATUS_COMPLETE) {
 269                      $compatiblepdf = pdf::ensure_pdf_compatible($file->get_destfile());
 270                  }
 271              } else {
 272                  $compatiblepdf = pdf::ensure_pdf_compatible($file);
 273              }
 274  
 275              if ($compatiblepdf) {
 276                  $compatiblepdfs[] = $compatiblepdf;
 277              }
 278          }
 279  
 280          $tmpdir = make_request_directory();
 281          $tmpfile = $tmpdir . '/' . document_services::COMBINED_PDF_FILENAME;
 282  
 283          try {
 284              $pagecount = $pdf->combine_pdfs($compatiblepdfs, $tmpfile);
 285              $pdf->Close();
 286          } catch (\Exception $e) {
 287              // Unable to combine the PDF.
 288              debugging('TCPDF could not process the pdf files:' . $e->getMessage(), DEBUG_DEVELOPER);
 289  
 290              $pdf->Close();
 291              return $this->mark_combination_failed();
 292          }
 293  
 294          // Verify the PDF.
 295          $verifypdf = new pdf();
 296          $verifypagecount = $verifypdf->load_pdf($tmpfile);
 297          $verifypdf->Close();
 298  
 299          if ($verifypagecount <= 0) {
 300              // No pages were found in the combined PDF.
 301              return $this->mark_combination_failed();
 302          }
 303  
 304          // Store the newly created file as a stored_file.
 305          $this->store_combined_file($tmpfile, $contextid, $itemid, ($currentstatus == self::STATUS_READY_PARTIAL));
 306  
 307          // Note the verified page count.
 308          $this->pagecount = $verifypagecount;
 309  
 310          return $this;
 311      }
 312  
 313      /**
 314       * Mark the combination attempt as having encountered a permanent failure.
 315       *
 316       * @return  $this
 317       */
 318      protected function mark_combination_failed() {
 319          $this->combinationstatus = self::STATUS_FAILED;
 320  
 321          return $this;
 322      }
 323  
 324      /**
 325       * Store the combined file in the file_storage API.
 326       *
 327       * @param   string $tmpfile The path to the file on disk to be stored.
 328       * @param   int $contextid The contextid for the file to be stored under
 329       * @param   int $itemid The itemid for the file to be stored under
 330       * @param   boolean $partial The combined pdf contains only some of the source files.
 331       * @return  $this
 332       */
 333      protected function store_combined_file($tmpfile, $contextid, $itemid, $partial = false) {
 334          // Store the file.
 335          $record = $this->get_stored_file_record($contextid, $itemid, $partial);
 336          $fs = get_file_storage();
 337  
 338          // Delete existing files first.
 339          $fs->delete_area_files($record->contextid, $record->component, $record->filearea, $record->itemid);
 340  
 341          // This was a combined pdf.
 342          $file = $fs->create_file_from_pathname($record, $tmpfile);
 343  
 344          $this->set_combined_file($file);
 345  
 346          return $this;
 347      }
 348  
 349      /**
 350       * Store the empty document file in the file_storage API.
 351       *
 352       * @param   int $contextid The contextid for the file to be stored under
 353       * @param   int $itemid The itemid for the file to be stored under
 354       * @return  $this
 355       */
 356      protected function store_empty_document($contextid, $itemid) {
 357          // Store the file.
 358          $record = $this->get_stored_file_record($contextid, $itemid);
 359          $fs = get_file_storage();
 360  
 361          // Delete existing files first.
 362          $fs->delete_area_files($record->contextid, $record->component, $record->filearea, $record->itemid);
 363  
 364          $file = $fs->create_file_from_string($record, base64_decode(document_services::BLANK_PDF_BASE64));
 365          $this->pagecount = 1;
 366  
 367          $this->set_combined_file($file);
 368  
 369          return $this;
 370      }
 371  
 372      /**
 373       * Get the total number of pages in the combined document.
 374       *
 375       * If there are no pages, or it is not yet possible to count them a
 376       * value of 0 is returned.
 377       *
 378       * @return  int
 379       */
 380      public function get_page_count() {
 381          if ($this->pagecount) {
 382              return $this->pagecount;
 383          }
 384  
 385          $status = $this->get_status();
 386  
 387          if ($status === self::STATUS_FAILED) {
 388              // The empty document will be returned.
 389              return 1;
 390          }
 391  
 392          if ($status !== self::STATUS_COMPLETE) {
 393              // No pages yet.
 394              return 0;
 395          }
 396  
 397          // Load the PDF to determine the page count.
 398          $temparea = make_request_directory();
 399          $tempsrc = $temparea . "/source.pdf";
 400          $this->get_combined_file()->copy_content_to($tempsrc);
 401  
 402          $pdf = new pdf();
 403          $pagecount = $pdf->load_pdf($tempsrc);
 404          $pdf->Close();
 405  
 406          if ($pagecount <= 0) {
 407              // Something went wrong. Return an empty page count again.
 408              return 0;
 409          }
 410  
 411          $this->pagecount = $pagecount;
 412          return $this->pagecount;
 413      }
 414  
 415      /**
 416       * Get the total number of documents to be combined.
 417       *
 418       * @return  int
 419       */
 420      public function get_document_count() {
 421          return count($this->sourcefiles);
 422      }
 423  
 424      /**
 425       * Helper to fetch the stored_file record.
 426       *
 427       * @param   int $contextid The contextid for the file to be stored under
 428       * @param   int $itemid The itemid for the file to be stored under
 429       * @param   boolean $partial The combined file contains only some of the source files.
 430       * @return  stdClass
 431       */
 432      protected function get_stored_file_record($contextid, $itemid, $partial = false) {
 433          $filearea = document_services::COMBINED_PDF_FILEAREA;
 434          if ($partial) {
 435              $filearea = document_services::PARTIAL_PDF_FILEAREA;
 436          }
 437          return (object) [
 438              'contextid' => $contextid,
 439              'component' => 'assignfeedback_editpdf',
 440              'filearea' => $filearea,
 441              'itemid' => $itemid,
 442              'filepath' => '/',
 443              'filename' => document_services::COMBINED_PDF_FILENAME,
 444          ];
 445      }
 446  }