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 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [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 ingest manager for the assignfeedback_editpdf plugin
  19   *
  20   * @package   assignfeedback_editpdf
  21   * @copyright 2012 Davo Smith
  22   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  namespace assignfeedback_editpdf;
  26  
  27  use DOMDocument;
  28  
  29  /**
  30   * Functions for generating the annotated pdf.
  31   *
  32   * This class controls the ingest of student submission files to a normalised
  33   * PDF 1.4 document with all submission files concatinated together. It also
  34   * provides the functions to generate a downloadable pdf with all comments and
  35   * annotations embedded.
  36   * @copyright 2012 Davo Smith
  37   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  38   */
  39  class document_services {
  40  
  41      /** Compoment name */
  42      const COMPONENT = "assignfeedback_editpdf";
  43      /** File area for generated pdf */
  44      const FINAL_PDF_FILEAREA = 'download';
  45      /** File area for combined pdf */
  46      const COMBINED_PDF_FILEAREA = 'combined';
  47      /** File area for partial combined pdf */
  48      const PARTIAL_PDF_FILEAREA = 'partial';
  49      /** File area for importing html */
  50      const IMPORT_HTML_FILEAREA = 'importhtml';
  51      /** File area for page images */
  52      const PAGE_IMAGE_FILEAREA = 'pages';
  53      /** File area for readonly page images */
  54      const PAGE_IMAGE_READONLY_FILEAREA = 'readonlypages';
  55      /** File area for the stamps */
  56      const STAMPS_FILEAREA = 'stamps';
  57      /** Filename for combined pdf */
  58      const COMBINED_PDF_FILENAME = 'combined.pdf';
  59      /**  Temporary place to save JPG Image to PDF file */
  60      const TMP_JPG_TO_PDF_FILEAREA = 'tmp_jpg_to_pdf';
  61      /**  Temporary place to save (Automatically) Rotated JPG FILE */
  62      const TMP_ROTATED_JPG_FILEAREA = 'tmp_rotated_jpg';
  63      /** Hash of blank pdf */
  64      const BLANK_PDF_HASH = '4c803c92c71f21b423d13de570c8a09e0a31c718';
  65  
  66      /** Base64 encoded blank pdf. This is the most reliable/fastest way to generate a blank pdf. */
  67      const BLANK_PDF_BASE64 = <<<EOD
  68  JVBERi0xLjQKJcOkw7zDtsOfCjIgMCBvYmoKPDwvTGVuZ3RoIDMgMCBSL0ZpbHRlci9GbGF0ZURl
  69  Y29kZT4+CnN0cmVhbQp4nDPQM1Qo5ypUMFAwALJMLU31jBQsTAz1LBSKUrnCtRTyuAIVAIcdB3IK
  70  ZW5kc3RyZWFtCmVuZG9iagoKMyAwIG9iago0MgplbmRvYmoKCjUgMCBvYmoKPDwKPj4KZW5kb2Jq
  71  Cgo2IDAgb2JqCjw8L0ZvbnQgNSAwIFIKL1Byb2NTZXRbL1BERi9UZXh0XQo+PgplbmRvYmoKCjEg
  72  MCBvYmoKPDwvVHlwZS9QYWdlL1BhcmVudCA0IDAgUi9SZXNvdXJjZXMgNiAwIFIvTWVkaWFCb3hb
  73  MCAwIDU5NSA4NDJdL0dyb3VwPDwvUy9UcmFuc3BhcmVuY3kvQ1MvRGV2aWNlUkdCL0kgdHJ1ZT4+
  74  L0NvbnRlbnRzIDIgMCBSPj4KZW5kb2JqCgo0IDAgb2JqCjw8L1R5cGUvUGFnZXMKL1Jlc291cmNl
  75  cyA2IDAgUgovTWVkaWFCb3hbIDAgMCA1OTUgODQyIF0KL0tpZHNbIDEgMCBSIF0KL0NvdW50IDE+
  76  PgplbmRvYmoKCjcgMCBvYmoKPDwvVHlwZS9DYXRhbG9nL1BhZ2VzIDQgMCBSCi9PcGVuQWN0aW9u
  77  WzEgMCBSIC9YWVogbnVsbCBudWxsIDBdCi9MYW5nKGVuLUFVKQo+PgplbmRvYmoKCjggMCBvYmoK
  78  PDwvQ3JlYXRvcjxGRUZGMDA1NzAwNzIwMDY5MDA3NDAwNjUwMDcyPgovUHJvZHVjZXI8RkVGRjAw
  79  NEMwMDY5MDA2MjAwNzIwMDY1MDA0RjAwNjYwMDY2MDA2OTAwNjMwMDY1MDAyMDAwMzQwMDJFMDAz
  80  ND4KL0NyZWF0aW9uRGF0ZShEOjIwMTYwMjI2MTMyMzE0KzA4JzAwJyk+PgplbmRvYmoKCnhyZWYK
  81  MCA5CjAwMDAwMDAwMDAgNjU1MzUgZiAKMDAwMDAwMDIyNiAwMDAwMCBuIAowMDAwMDAwMDE5IDAw
  82  MDAwIG4gCjAwMDAwMDAxMzIgMDAwMDAgbiAKMDAwMDAwMDM2OCAwMDAwMCBuIAowMDAwMDAwMTUx
  83  IDAwMDAwIG4gCjAwMDAwMDAxNzMgMDAwMDAgbiAKMDAwMDAwMDQ2NiAwMDAwMCBuIAowMDAwMDAw
  84  NTYyIDAwMDAwIG4gCnRyYWlsZXIKPDwvU2l6ZSA5L1Jvb3QgNyAwIFIKL0luZm8gOCAwIFIKL0lE
  85  IFsgPEJDN0REQUQwRDQyOTQ1OTQ2OUU4NzJCMjI1ODUyNkU4Pgo8QkM3RERBRDBENDI5NDU5NDY5
  86  RTg3MkIyMjU4NTI2RTg+IF0KL0RvY0NoZWNrc3VtIC9BNTYwMEZCMDAzRURCRTg0MTNBNTk3RTZF
  87  MURDQzJBRgo+PgpzdGFydHhyZWYKNzM2CiUlRU9GCg==
  88  EOD;
  89  
  90      /**
  91       * This function will take an int or an assignment instance and
  92       * return an assignment instance. It is just for convenience.
  93       * @param int|\assign $assignment
  94       * @return assign
  95       */
  96      private static function get_assignment_from_param($assignment) {
  97          global $CFG;
  98  
  99          require_once($CFG->dirroot . '/mod/assign/locallib.php');
 100  
 101          if (!is_object($assignment)) {
 102              $cm = get_coursemodule_from_instance('assign', $assignment, 0, false, MUST_EXIST);
 103              $context = \context_module::instance($cm->id);
 104  
 105              $assignment = new \assign($context, null, null);
 106          }
 107          return $assignment;
 108      }
 109  
 110      /**
 111       * Get a hash that will be unique and can be used in a path name.
 112       * @param int|\assign $assignment
 113       * @param int $userid
 114       * @param int $attemptnumber (-1 means latest attempt)
 115       */
 116      private static function hash($assignment, $userid, $attemptnumber) {
 117          if (is_object($assignment)) {
 118              $assignmentid = $assignment->get_instance()->id;
 119          } else {
 120              $assignmentid = $assignment;
 121          }
 122          return sha1($assignmentid . '_' . $userid . '_' . $attemptnumber);
 123      }
 124  
 125      /**
 126       * Use a DOM parser to accurately replace images with their alt text.
 127       * @param string $html
 128       * @return string New html with no image tags.
 129       */
 130      protected static function strip_images($html) {
 131          // Load HTML and suppress any parsing errors (DOMDocument->loadHTML() does not current support HTML5 tags).
 132          $dom = new DOMDocument();
 133          libxml_use_internal_errors(true);
 134          $dom->loadHTML('<?xml version="1.0" encoding="UTF-8" ?>' . $html);
 135          libxml_clear_errors();
 136  
 137          // Find all img tags.
 138          if ($imgnodes = $dom->getElementsByTagName('img')) {
 139              // Replace img nodes with the img alt text without overriding DOM elements.
 140              for ($i = ($imgnodes->length - 1); $i >= 0; $i--) {
 141                  $imgnode = $imgnodes->item($i);
 142                  $alt = ($imgnode->hasAttribute('alt')) ? ' [ ' . $imgnode->getAttribute('alt') . ' ] ' : ' ';
 143                  $textnode = $dom->createTextNode($alt);
 144  
 145                  $imgnode->parentNode->replaceChild($textnode, $imgnode);
 146              }
 147          }
 148          $count = 1;
 149          return str_replace("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>", "", $dom->saveHTML(), $count);
 150      }
 151  
 152      /**
 153       * This function will search for all files that can be converted
 154       * and concatinated into a PDF (1.4) - for any submission plugin
 155       * for this students attempt.
 156       *
 157       * @param int|\assign $assignment
 158       * @param int $userid
 159       * @param int $attemptnumber (-1 means latest attempt)
 160       * @return combined_document
 161       */
 162      protected static function list_compatible_submission_files_for_attempt($assignment, $userid, $attemptnumber) {
 163          global $USER, $DB;
 164  
 165          $assignment = self::get_assignment_from_param($assignment);
 166  
 167          // Capability checks.
 168          if (!$assignment->can_view_submission($userid)) {
 169              throw new \moodle_exception('nopermission');
 170          }
 171  
 172          $files = array();
 173  
 174          if ($assignment->get_instance()->teamsubmission) {
 175              $submission = $assignment->get_group_submission($userid, 0, false, $attemptnumber);
 176          } else {
 177              $submission = $assignment->get_user_submission($userid, false, $attemptnumber);
 178          }
 179          $user = $DB->get_record('user', array('id' => $userid));
 180  
 181          // User has not submitted anything yet.
 182          if (!$submission) {
 183              return new combined_document();
 184          }
 185  
 186          $fs = get_file_storage();
 187          $converter = new \core_files\converter();
 188          // Ask each plugin for it's list of files.
 189          foreach ($assignment->get_submission_plugins() as $plugin) {
 190              if ($plugin->is_enabled() && $plugin->is_visible()) {
 191                  $pluginfiles = $plugin->get_files($submission, $user);
 192                  foreach ($pluginfiles as $filename => $file) {
 193                      if ($file instanceof \stored_file) {
 194                          $mimetype = $file->get_mimetype();
 195                          // PDF File, no conversion required.
 196                          if ($mimetype === 'application/pdf') {
 197                              $files[$filename] = $file;
 198                          } else if ($plugin->allow_image_conversion() && $mimetype === "image/jpeg") {
 199                              // Rotates image based on the EXIF value.
 200                              list ($rotateddata, $size) = $file->rotate_image();
 201                              if ($rotateddata) {
 202                                  $file = self::save_rotated_image_file($assignment, $userid, $attemptnumber,
 203                                      $rotateddata, $filename);
 204                              }
 205                              // Save as PDF file if there is no available converter.
 206                              if (!$converter->can_convert_format_to('jpg', 'pdf')) {
 207                                  $pdffile = self::save_jpg_to_pdf($assignment, $userid, $attemptnumber, $file, $size);
 208                                  if ($pdffile) {
 209                                      $files[$filename] = $pdffile;
 210                                  }
 211                              }
 212                          }
 213                          // The file has not been converted to PDF, try to convert it to PDF.
 214                          if (!isset($files[$filename])
 215                              && $convertedfile = $converter->start_conversion($file, 'pdf')) {
 216                              $files[$filename] = $convertedfile;
 217                          }
 218                      } else if ($converter->can_convert_format_to('html', 'pdf')) {
 219                          // Create a tmp stored_file from this html string.
 220                          $file = reset($file);
 221                          // Strip image tags, because they will not be resolvable.
 222                          $file = self::strip_images($file);
 223                          $record = new \stdClass();
 224                          $record->contextid = $assignment->get_context()->id;
 225                          $record->component = 'assignfeedback_editpdf';
 226                          $record->filearea = self::IMPORT_HTML_FILEAREA;
 227                          $record->itemid = $submission->id;
 228                          $record->filepath = '/';
 229                          $record->filename = $plugin->get_type() . '-' . $filename;
 230  
 231                          $htmlfile = $fs->get_file($record->contextid,
 232                                  $record->component,
 233                                  $record->filearea,
 234                                  $record->itemid,
 235                                  $record->filepath,
 236                                  $record->filename);
 237  
 238                          $newhash = sha1($file);
 239  
 240                          // If the file exists, and the content hash doesn't match, remove it.
 241                          if ($htmlfile && $newhash !== $htmlfile->get_contenthash()) {
 242                              $htmlfile->delete();
 243                              $htmlfile = false;
 244                          }
 245  
 246                          // If the file doesn't exist, or if it was removed above, create a new one.
 247                          if (!$htmlfile) {
 248                              $htmlfile = $fs->create_file_from_string($record, $file);
 249                          }
 250  
 251                          $convertedfile = $converter->start_conversion($htmlfile, 'pdf');
 252  
 253                          if ($convertedfile) {
 254                              $files[$filename] = $convertedfile;
 255                          }
 256                      }
 257                  }
 258              }
 259          }
 260          $combineddocument = new combined_document();
 261          $combineddocument->set_source_files($files);
 262  
 263          return $combineddocument;
 264      }
 265  
 266      /**
 267       * Fetch the current combined document ready for state checking.
 268       *
 269       * @param int|\assign $assignment
 270       * @param int $userid
 271       * @param int $attemptnumber (-1 means latest attempt)
 272       * @return combined_document
 273       */
 274      public static function get_combined_document_for_attempt($assignment, $userid, $attemptnumber) {
 275          global $USER, $DB;
 276  
 277          $assignment = self::get_assignment_from_param($assignment);
 278  
 279          // Capability checks.
 280          if (!$assignment->can_view_submission($userid)) {
 281              throw new \moodle_exception('nopermissiontoaccesspage', 'error');
 282          }
 283  
 284          $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
 285          if ($assignment->get_instance()->teamsubmission) {
 286              $submission = $assignment->get_group_submission($userid, 0, false, $attemptnumber);
 287          } else {
 288              $submission = $assignment->get_user_submission($userid, false, $attemptnumber);
 289          }
 290  
 291          $contextid = $assignment->get_context()->id;
 292          $component = 'assignfeedback_editpdf';
 293          $filearea = self::COMBINED_PDF_FILEAREA;
 294          $partialfilearea = self::PARTIAL_PDF_FILEAREA;
 295          $itemid = $grade->id;
 296          $filepath = '/';
 297          $filename = self::COMBINED_PDF_FILENAME;
 298          $fs = get_file_storage();
 299  
 300          $partialpdf = $fs->get_file($contextid, $component, $partialfilearea, $itemid, $filepath, $filename);
 301          if (!empty($partialpdf)) {
 302              $combinedpdf = $partialpdf;
 303          } else {
 304              $combinedpdf = $fs->get_file($contextid, $component, $filearea, $itemid, $filepath, $filename);
 305          }
 306  
 307          if ($combinedpdf && $submission) {
 308              if ($combinedpdf->get_timemodified() < $submission->timemodified) {
 309                  // The submission has been updated since the PDF was generated.
 310                  $combinedpdf = false;
 311              } else if ($combinedpdf->get_contenthash() == self::BLANK_PDF_HASH) {
 312                  // The PDF is for a blank page.
 313                  $combinedpdf = false;
 314              }
 315          }
 316  
 317          if (empty($combinedpdf)) {
 318              // The combined PDF does not exist yet. Return the list of files to be combined.
 319              return self::list_compatible_submission_files_for_attempt($assignment, $userid, $attemptnumber);
 320          } else {
 321              // The combined PDF aleady exists. Return it in a new combined_document object.
 322              $combineddocument = new combined_document();
 323              return $combineddocument->set_combined_file($combinedpdf);
 324          }
 325      }
 326  
 327      /**
 328       * This function return the combined pdf for all valid submission files.
 329       *
 330       * @param int|\assign $assignment
 331       * @param int $userid
 332       * @param int $attemptnumber (-1 means latest attempt)
 333       * @return combined_document
 334       */
 335      public static function get_combined_pdf_for_attempt($assignment, $userid, $attemptnumber) {
 336          $document = self::get_combined_document_for_attempt($assignment, $userid, $attemptnumber);
 337  
 338          if ($document->get_status() === combined_document::STATUS_COMPLETE) {
 339              // The combined document is already ready.
 340              return $document;
 341          } else {
 342              // Attempt to combined the files in the document.
 343              $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
 344              $document->combine_files($assignment->get_context()->id, $grade->id);
 345              return $document;
 346          }
 347      }
 348  
 349      /**
 350       * This function will return the number of pages of a pdf.
 351       *
 352       * @param int|\assign $assignment
 353       * @param int $userid
 354       * @param int $attemptnumber (-1 means latest attempt)
 355       * @param bool $readonly When true we get the number of pages for the readonly version.
 356       * @return int number of pages
 357       */
 358      public static function page_number_for_attempt($assignment, $userid, $attemptnumber, $readonly = false) {
 359          global $CFG;
 360  
 361          require_once($CFG->libdir . '/pdflib.php');
 362  
 363          $assignment = self::get_assignment_from_param($assignment);
 364  
 365          if (!$assignment->can_view_submission($userid)) {
 366              throw new \moodle_exception('nopermission');
 367          }
 368  
 369          // When in readonly we can return the number of images in the DB because they should already exist,
 370          // if for some reason they do not, then we proceed as for the normal version.
 371          if ($readonly) {
 372              $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
 373              $fs = get_file_storage();
 374              $files = $fs->get_directory_files($assignment->get_context()->id, 'assignfeedback_editpdf',
 375                  self::PAGE_IMAGE_READONLY_FILEAREA, $grade->id, '/');
 376              $pagecount = count($files);
 377              if ($pagecount > 0) {
 378                  return $pagecount;
 379              }
 380          }
 381  
 382          // Get a combined pdf file from all submitted pdf files.
 383          $document = self::get_combined_pdf_for_attempt($assignment, $userid, $attemptnumber);
 384          return $document->get_page_count();
 385      }
 386  
 387      /**
 388       * This function will generate and return a list of the page images from a pdf.
 389       * @param int|\assign $assignment
 390       * @param int $userid
 391       * @param int $attemptnumber (-1 means latest attempt)
 392       * @param bool $resetrotation check if need to reset page rotation information
 393       * @return array(stored_file)
 394       */
 395      protected static function generate_page_images_for_attempt($assignment, $userid, $attemptnumber, $resetrotation = true) {
 396          global $CFG;
 397  
 398          require_once($CFG->libdir . '/pdflib.php');
 399  
 400          $assignment = self::get_assignment_from_param($assignment);
 401  
 402          if (!$assignment->can_view_submission($userid)) {
 403              throw new \moodle_exception('nopermission');
 404          }
 405  
 406          // Need to generate the page images - first get a combined pdf.
 407          $document = self::get_combined_pdf_for_attempt($assignment, $userid, $attemptnumber);
 408  
 409          $status = $document->get_status();
 410          if ($status === combined_document::STATUS_FAILED) {
 411              throw new \moodle_exception('Could not generate combined pdf.');
 412          } else if ($status === combined_document::STATUS_PENDING_INPUT) {
 413              // The conversion is still in progress.
 414              return [];
 415          }
 416  
 417          $tmpdir = \make_temp_directory('assignfeedback_editpdf/pageimages/' . self::hash($assignment, $userid, $attemptnumber));
 418          $combined = $tmpdir . '/' . self::COMBINED_PDF_FILENAME;
 419  
 420          $document->get_combined_file()->copy_content_to($combined); // Copy the file.
 421  
 422          $pdf = new pdf();
 423  
 424          $pdf->set_image_folder($tmpdir);
 425          $pagecount = $pdf->set_pdf($combined);
 426  
 427          $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
 428  
 429          $record = new \stdClass();
 430          $record->contextid = $assignment->get_context()->id;
 431          $record->component = 'assignfeedback_editpdf';
 432          $record->filearea = self::PAGE_IMAGE_FILEAREA;
 433          $record->itemid = $grade->id;
 434          $record->filepath = '/';
 435          $fs = get_file_storage();
 436  
 437          // Remove the existing content of the filearea.
 438          $fs->delete_area_files($record->contextid, $record->component, $record->filearea, $record->itemid);
 439  
 440          $files = array();
 441          $images = $pdf->get_images();
 442          for ($i = 0; $i < $pagecount; $i++) {
 443              try {
 444                  if (empty($images[$i])) {
 445                      throw new \moodle_exception('error image');
 446                  }
 447                  $image = $images[$i];
 448                  if (!$resetrotation) {
 449                      $pagerotation = page_editor::get_page_rotation($grade->id, $i);
 450                      $degree = !empty($pagerotation) ? $pagerotation->degree : 0;
 451                      if ($degree != 0) {
 452                          $filepath = $tmpdir . '/' . $image;
 453                          $imageresource = imagecreatefrompng($filepath);
 454                          $content = imagerotate($imageresource, $degree, 0);
 455                          imagepng($content, $filepath);
 456                      }
 457                  }
 458              } catch (\moodle_exception $e) {
 459                  // We catch only moodle_exception here as other exceptions indicate issue with setup not the pdf.
 460                  $image = pdf::get_error_image($tmpdir, $i);
 461              }
 462              $record->filename = basename($image);
 463              $files[$i] = $fs->create_file_from_pathname($record, $tmpdir . '/' . $image);
 464              @unlink($tmpdir . '/' . $image);
 465              // Set page rotation default value.
 466              if (!empty($files[$i])) {
 467                  if ($resetrotation) {
 468                      page_editor::set_page_rotation($grade->id, $i, false, $files[$i]->get_pathnamehash());
 469                  }
 470              }
 471          }
 472          $pdf->Close(); // PDF loaded and never saved/outputted needs to be closed.
 473  
 474          @unlink($combined);
 475          @rmdir($tmpdir);
 476  
 477          return $files;
 478      }
 479  
 480      /**
 481       * This function returns a list of the page images from a pdf.
 482       *
 483       * The readonly version is different than the normal one. The readonly version contains a copy
 484       * of the pages in the state they were when the PDF was annotated, by doing so we prevent the
 485       * the pages that are displayed to change as soon as the submission changes.
 486       *
 487       * Though there is an edge case, if the PDF was annotated before MDL-45580, then it is possible
 488       * that we do not find any readonly version of the pages. In that case, we will get the normal
 489       * pages and copy them to the readonly area. This ensures that the pages will remain in that
 490       * state until the submission is updated. When the normal files do not exist, we throw an exception
 491       * because the readonly pages should only ever be displayed after a teacher has annotated the PDF,
 492       * they would not exist until they do.
 493       *
 494       * @param int|\assign $assignment
 495       * @param int $userid
 496       * @param int $attemptnumber (-1 means latest attempt)
 497       * @param bool $readonly If true, then we are requesting the readonly version.
 498       * @return array(stored_file)
 499       */
 500      public static function get_page_images_for_attempt($assignment, $userid, $attemptnumber, $readonly = false) {
 501          global $DB;
 502  
 503          $assignment = self::get_assignment_from_param($assignment);
 504  
 505          if (!$assignment->can_view_submission($userid)) {
 506              throw new \moodle_exception('nopermission');
 507          }
 508  
 509          if ($assignment->get_instance()->teamsubmission) {
 510              $submission = $assignment->get_group_submission($userid, 0, false, $attemptnumber);
 511          } else {
 512              $submission = $assignment->get_user_submission($userid, false, $attemptnumber);
 513          }
 514          $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
 515  
 516          $contextid = $assignment->get_context()->id;
 517          $component = 'assignfeedback_editpdf';
 518          $itemid = $grade->id;
 519          $filepath = '/';
 520          $filearea = self::PAGE_IMAGE_FILEAREA;
 521  
 522          $fs = get_file_storage();
 523  
 524          // If we are after the readonly pages...
 525          if ($readonly) {
 526              $filearea = self::PAGE_IMAGE_READONLY_FILEAREA;
 527              if ($fs->is_area_empty($contextid, $component, $filearea, $itemid)) {
 528                  // We have a problem here, we were supposed to find the files.
 529                  // Attempt to re-generate the pages from the combined images.
 530                  self::generate_page_images_for_attempt($assignment, $userid, $attemptnumber);
 531                  self::copy_pages_to_readonly_area($assignment, $grade);
 532              }
 533          }
 534  
 535          $files = $fs->get_directory_files($contextid, $component, $filearea, $itemid, $filepath);
 536  
 537          $pages = array();
 538          $resetrotation = false;
 539          if (!empty($files)) {
 540              $first = reset($files);
 541              $pagemodified = $first->get_timemodified();
 542              // Check that we don't just have a single blank page. The hash of a blank page image can vary with
 543              // the version of ghostscript used, so we need to examine the combined pdf it was generated from.
 544              $blankpage = false;
 545              if (!$readonly && count($files) == 1) {
 546                  $pdfarea = self::COMBINED_PDF_FILEAREA;
 547                  $pdfname = self::COMBINED_PDF_FILENAME;
 548                  if ($pdf = $fs->get_file($contextid, $component, $pdfarea, $itemid, $filepath, $pdfname)) {
 549                      // The combined pdf may have a different hash if it has been regenerated since the page
 550                      // image was created. However if this is the case the page image will be stale anyway.
 551                      if ($pdf->get_contenthash() == self::BLANK_PDF_HASH || $pagemodified < $pdf->get_timemodified()) {
 552                          $blankpage = true;
 553                      }
 554                  }
 555              }
 556              if (!$readonly && ($pagemodified < $submission->timemodified || $blankpage)) {
 557                  // Image files are stale, we need to regenerate them, except in readonly mode.
 558                  // We also need to remove the draft annotations and comments associated with this attempt.
 559                  $fs->delete_area_files($contextid, $component, $filearea, $itemid);
 560                  page_editor::delete_draft_content($itemid);
 561                  $files = array();
 562                  $resetrotation = true;
 563              } else {
 564  
 565                  // Need to reorder the files following their name.
 566                  // because get_directory_files() return a different order than generate_page_images_for_attempt().
 567                  foreach ($files as $file) {
 568                      // Extract the page number from the file name image_pageXXXX.png.
 569                      preg_match('/page([\d]+)\./', $file->get_filename(), $matches);
 570                      if (empty($matches) or !is_numeric($matches[1])) {
 571                          throw new \coding_exception("'" . $file->get_filename()
 572                              . "' file hasn't the expected format filename: image_pageXXXX.png.");
 573                      }
 574                      $pagenumber = (int)$matches[1];
 575  
 576                      // Save the page in the ordered array.
 577                      $pages[$pagenumber] = $file;
 578                  }
 579                  ksort($pages);
 580              }
 581          }
 582  
 583          // This should never happen, there should be a version of the pages available
 584          // whenever we are requesting the readonly version.
 585          if (empty($pages) && $readonly) {
 586              throw new \moodle_exception('Could not find readonly pages for grade ' . $grade->id);
 587          }
 588  
 589          // There are two situations where the number of page images generated does not
 590          // match the number of pages in the PDF:
 591          //
 592          // 1. The document conversion adhoc task was interrupted somehow (node died, solar flare, etc)
 593          // 2. The submission has been updated by the student
 594          //
 595          // In the case of 1. we need to regenerate the pages, see MDL-66626.
 596          // In the case of 2. we should do nothing, see MDL-45580.
 597          //
 598          // To differentiate between 1. and 2. we can check if the submission has been modified since the
 599          // pages were generated. If it has, then we're in situation 2.
 600          $totalpagesforattempt = self::page_number_for_attempt($assignment, $userid, $attemptnumber, false);
 601          $submissionmodified = isset($pagemodified) && $submission->timemodified > $pagemodified;
 602          if (empty($pages) || (count($pages) != $totalpagesforattempt && !$submissionmodified)) {
 603              $pages = self::generate_page_images_for_attempt($assignment, $userid, $attemptnumber, $resetrotation);
 604          }
 605  
 606          return $pages;
 607      }
 608  
 609      /**
 610       * This function returns sensible filename for a feedback file.
 611       * @param int|\assign $assignment
 612       * @param int $userid
 613       * @param int $attemptnumber (-1 means latest attempt)
 614       * @return string
 615       */
 616      protected static function get_downloadable_feedback_filename($assignment, $userid, $attemptnumber) {
 617          global $DB;
 618  
 619          $assignment = self::get_assignment_from_param($assignment);
 620  
 621          $groupmode = groups_get_activity_groupmode($assignment->get_course_module());
 622          $groupname = '';
 623          if ($groupmode) {
 624              $groupid = groups_get_activity_group($assignment->get_course_module(), true);
 625              $groupname = groups_get_group_name($groupid).'-';
 626          }
 627          if ($groupname == '-') {
 628              $groupname = '';
 629          }
 630          $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
 631          $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
 632  
 633          if ($assignment->is_blind_marking()) {
 634              $prefix = $groupname . get_string('participant', 'assign');
 635              $prefix = str_replace('_', ' ', $prefix);
 636              $prefix = clean_filename($prefix . '_' . $assignment->get_uniqueid_for_user($userid) . '_');
 637          } else {
 638              $prefix = $groupname . fullname($user);
 639              $prefix = str_replace('_', ' ', $prefix);
 640              $prefix = clean_filename($prefix . '_' . $assignment->get_uniqueid_for_user($userid) . '_');
 641          }
 642          $prefix .= $grade->attemptnumber;
 643  
 644          return $prefix . '.pdf';
 645      }
 646  
 647      /**
 648       * This function takes the combined pdf and embeds all the comments and annotations.
 649       *
 650       * This also moves the annotations and comments from drafts to not drafts. And it will
 651       * copy all the images stored to the readonly area, so that they can be viewed online, and
 652       * not be overwritten when a new submission is sent.
 653       *
 654       * @param int|\assign $assignment
 655       * @param int $userid
 656       * @param int $attemptnumber (-1 means latest attempt)
 657       * @return stored_file
 658       */
 659      public static function generate_feedback_document($assignment, $userid, $attemptnumber) {
 660  
 661          $assignment = self::get_assignment_from_param($assignment);
 662  
 663          if (!$assignment->can_view_submission($userid)) {
 664              throw new \moodle_exception('nopermission');
 665          }
 666          if (!$assignment->can_grade()) {
 667              throw new \moodle_exception('nopermission');
 668          }
 669  
 670          // Need to generate the page images - first get a combined pdf.
 671          $document = self::get_combined_pdf_for_attempt($assignment, $userid, $attemptnumber);
 672  
 673          $status = $document->get_status();
 674          if ($status === combined_document::STATUS_FAILED) {
 675              throw new \moodle_exception('Could not generate combined pdf.');
 676          } else if ($status === combined_document::STATUS_PENDING_INPUT) {
 677              // The conversion is still in progress.
 678              return false;
 679          }
 680  
 681          $file = $document->get_combined_file();
 682  
 683          $tmpdir = make_temp_directory('assignfeedback_editpdf/final/' . self::hash($assignment, $userid, $attemptnumber));
 684          $combined = $tmpdir . '/' . self::COMBINED_PDF_FILENAME;
 685          $file->copy_content_to($combined); // Copy the file.
 686  
 687          $pdf = new pdf();
 688  
 689          $fs = get_file_storage();
 690          $stamptmpdir = make_temp_directory('assignfeedback_editpdf/stamps/' . self::hash($assignment, $userid, $attemptnumber));
 691          $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
 692          // Copy any new stamps to this instance.
 693          if ($files = $fs->get_area_files($assignment->get_context()->id,
 694                                           'assignfeedback_editpdf',
 695                                           'stamps',
 696                                           $grade->id,
 697                                           "filename",
 698                                           false)) {
 699              foreach ($files as $file) {
 700                  $filename = $stamptmpdir . '/' . $file->get_filename();
 701                  $file->copy_content_to($filename); // Copy the file.
 702              }
 703          }
 704  
 705          $pagecount = $pdf->set_pdf($combined);
 706          $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
 707          page_editor::release_drafts($grade->id);
 708  
 709          $allcomments = array();
 710  
 711          for ($i = 0; $i < $pagecount; $i++) {
 712              $pagerotation = page_editor::get_page_rotation($grade->id, $i);
 713              $pagemargin = $pdf->getBreakMargin();
 714              $autopagebreak = $pdf->getAutoPageBreak();
 715              if (empty($pagerotation) || !$pagerotation->isrotated) {
 716                  $pdf->copy_page();
 717              } else {
 718                  $rotatedimagefile = $fs->get_file_by_hash($pagerotation->pathnamehash);
 719                  if (empty($rotatedimagefile)) {
 720                      $pdf->copy_page();
 721                  } else {
 722                      $pdf->add_image_page($rotatedimagefile);
 723                  }
 724              }
 725  
 726              $comments = page_editor::get_comments($grade->id, $i, false);
 727              $annotations = page_editor::get_annotations($grade->id, $i, false);
 728  
 729              if (!empty($comments)) {
 730                  $allcomments[$i] = $comments;
 731              }
 732  
 733              foreach ($annotations as $annotation) {
 734                  $pdf->add_annotation($annotation->x,
 735                                       $annotation->y,
 736                                       $annotation->endx,
 737                                       $annotation->endy,
 738                                       $annotation->colour,
 739                                       $annotation->type,
 740                                       $annotation->path,
 741                                       $stamptmpdir);
 742              }
 743              $pdf->SetAutoPageBreak($autopagebreak, $pagemargin);
 744              $pdf->setPageMark();
 745          }
 746  
 747          if (!empty($allcomments)) {
 748              // Append all comments to the end of the document.
 749              $links = $pdf->append_comments($allcomments);
 750              // Add the comment markers with links.
 751              foreach ($allcomments as $pageno => $comments) {
 752                  foreach ($comments as $index => $comment) {
 753                      $pdf->add_comment_marker($comment->pageno, $index, $comment->x, $comment->y, $links[$pageno][$index],
 754                              $comment->colour);
 755                  }
 756              }
 757          }
 758  
 759          fulldelete($stamptmpdir);
 760  
 761          $filename = self::get_downloadable_feedback_filename($assignment, $userid, $attemptnumber);
 762          $filename = clean_param($filename, PARAM_FILE);
 763  
 764          $generatedpdf = $tmpdir . '/' . $filename;
 765          $pdf->save_pdf($generatedpdf);
 766  
 767          $record = new \stdClass();
 768  
 769          $record->contextid = $assignment->get_context()->id;
 770          $record->component = 'assignfeedback_editpdf';
 771          $record->filearea = self::FINAL_PDF_FILEAREA;
 772          $record->itemid = $grade->id;
 773          $record->filepath = '/';
 774          $record->filename = $filename;
 775  
 776          // Only keep one current version of the generated pdf.
 777          $fs->delete_area_files($record->contextid, $record->component, $record->filearea, $record->itemid);
 778  
 779          $file = $fs->create_file_from_pathname($record, $generatedpdf);
 780  
 781          // Cleanup.
 782          @unlink($generatedpdf);
 783          @unlink($combined);
 784          @rmdir($tmpdir);
 785  
 786          self::copy_pages_to_readonly_area($assignment, $grade);
 787  
 788          return $file;
 789      }
 790  
 791      /**
 792       * Copy the pages image to the readonly area.
 793       *
 794       * @param int|\assign $assignment The assignment.
 795       * @param \stdClass $grade The grade record.
 796       * @return void
 797       */
 798      public static function copy_pages_to_readonly_area($assignment, $grade) {
 799          $fs = get_file_storage();
 800          $assignment = self::get_assignment_from_param($assignment);
 801          $contextid = $assignment->get_context()->id;
 802          $component = 'assignfeedback_editpdf';
 803          $itemid = $grade->id;
 804  
 805          // Get all the pages.
 806          $originalfiles = $fs->get_area_files($contextid, $component, self::PAGE_IMAGE_FILEAREA, $itemid);
 807          if (empty($originalfiles)) {
 808              // Nothing to do here...
 809              return;
 810          }
 811  
 812          // Delete the old readonly files.
 813          $fs->delete_area_files($contextid, $component, self::PAGE_IMAGE_READONLY_FILEAREA, $itemid);
 814  
 815          // Do the copying.
 816          foreach ($originalfiles as $originalfile) {
 817              $fs->create_file_from_storedfile(array('filearea' => self::PAGE_IMAGE_READONLY_FILEAREA), $originalfile);
 818          }
 819      }
 820  
 821      /**
 822       * This function returns the generated pdf (if it exists).
 823       * @param int|\assign $assignment
 824       * @param int $userid
 825       * @param int $attemptnumber (-1 means latest attempt)
 826       * @return stored_file
 827       */
 828      public static function get_feedback_document($assignment, $userid, $attemptnumber) {
 829  
 830          $assignment = self::get_assignment_from_param($assignment);
 831  
 832          if (!$assignment->can_view_submission($userid)) {
 833              throw new \moodle_exception('nopermission');
 834          }
 835  
 836          $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
 837  
 838          $contextid = $assignment->get_context()->id;
 839          $component = 'assignfeedback_editpdf';
 840          $filearea = self::FINAL_PDF_FILEAREA;
 841          $itemid = $grade->id;
 842          $filepath = '/';
 843  
 844          $fs = get_file_storage();
 845          $files = $fs->get_area_files($contextid,
 846                                       $component,
 847                                       $filearea,
 848                                       $itemid,
 849                                       "itemid, filepath, filename",
 850                                       false);
 851          if ($files) {
 852              return reset($files);
 853          }
 854          return false;
 855      }
 856  
 857      /**
 858       * This function deletes the generated pdf for a student.
 859       * @param int|\assign $assignment
 860       * @param int $userid
 861       * @param int $attemptnumber (-1 means latest attempt)
 862       * @return bool
 863       */
 864      public static function delete_feedback_document($assignment, $userid, $attemptnumber) {
 865  
 866          $assignment = self::get_assignment_from_param($assignment);
 867  
 868          if (!$assignment->can_view_submission($userid)) {
 869              throw new \moodle_exception('nopermission');
 870          }
 871          if (!$assignment->can_grade()) {
 872              throw new \moodle_exception('nopermission');
 873          }
 874  
 875          $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
 876  
 877          $contextid = $assignment->get_context()->id;
 878          $component = 'assignfeedback_editpdf';
 879          $filearea = self::FINAL_PDF_FILEAREA;
 880          $itemid = $grade->id;
 881  
 882          $fs = get_file_storage();
 883          return $fs->delete_area_files($contextid, $component, $filearea, $itemid);
 884      }
 885  
 886      /**
 887       * Get All files in a File area
 888       * @param int|\assign $assignment Assignment
 889       * @param int $userid User ID
 890       * @param int $attemptnumber Attempt Number
 891       * @param string $filearea File Area
 892       * @param string $filepath File Path
 893       * @return array
 894       */
 895      private static function get_files($assignment, $userid, $attemptnumber, $filearea, $filepath = '/') {
 896          $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
 897          $itemid = $grade->id;
 898          $contextid = $assignment->get_context()->id;
 899          $component = self::COMPONENT;
 900          $fs = get_file_storage();
 901          $files = $fs->get_directory_files($contextid, $component, $filearea, $itemid, $filepath);
 902          return $files;
 903      }
 904  
 905      /**
 906       * Save file.
 907       * @param int|\assign $assignment Assignment
 908       * @param int $userid User ID
 909       * @param int $attemptnumber Attempt Number
 910       * @param string $filearea File Area
 911       * @param string $newfilepath File Path
 912       * @param string $storedfilepath stored file path
 913       * @return \stored_file
 914       * @throws \file_exception
 915       * @throws \stored_file_creation_exception
 916       */
 917      private static function save_file($assignment, $userid, $attemptnumber, $filearea, $newfilepath, $storedfilepath = '/') {
 918          $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
 919          $itemid = $grade->id;
 920          $contextid = $assignment->get_context()->id;
 921  
 922          $record = new \stdClass();
 923          $record->contextid = $contextid;
 924          $record->component = self::COMPONENT;
 925          $record->filearea = $filearea;
 926          $record->itemid = $itemid;
 927          $record->filepath = $storedfilepath;
 928          $record->filename = basename($newfilepath);
 929  
 930          $fs = get_file_storage();
 931  
 932          $oldfile = $fs->get_file($record->contextid, $record->component, $record->filearea,
 933              $record->itemid, $record->filepath, $record->filename);
 934  
 935          if ($oldfile) {
 936              $newhash = \file_storage::hash_from_path($newfilepath);
 937              if ($newhash === $oldfile->get_contenthash()) {
 938                  // Use existing file if contenthash match.
 939                  return $oldfile;
 940              }
 941              // Delete existing file.
 942              $oldfile->delete();
 943          }
 944  
 945          return $fs->create_file_from_pathname($record, $newfilepath);
 946      }
 947  
 948      /**
 949       * This function rotate a page, and mark the page as rotated.
 950       * @param int|\assign $assignment Assignment
 951       * @param int $userid User ID
 952       * @param int $attemptnumber Attempt Number
 953       * @param int $index Index of Current Page
 954       * @param bool $rotateleft To determine whether the page is rotated left or right.
 955       * @return null|\stored_file return rotated File
 956       * @throws \coding_exception
 957       * @throws \file_exception
 958       * @throws \moodle_exception
 959       * @throws \stored_file_creation_exception
 960       */
 961      public static function rotate_page($assignment, $userid, $attemptnumber, $index, $rotateleft) {
 962          $assignment = self::get_assignment_from_param($assignment);
 963          $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
 964          // Check permission.
 965          if (!$assignment->can_view_submission($userid)) {
 966              throw new \moodle_exception('nopermission');
 967          }
 968  
 969          $filearea = self::PAGE_IMAGE_FILEAREA;
 970          $files = self::get_files($assignment, $userid, $attemptnumber, $filearea);
 971          if (!empty($files)) {
 972              foreach ($files as $file) {
 973                  preg_match('/' . pdf::IMAGE_PAGE . '([\d]+)\./', $file->get_filename(), $matches);
 974                  if (empty($matches) or !is_numeric($matches[1])) {
 975                      throw new \coding_exception("'" . $file->get_filename()
 976                          . "' file hasn't the expected format filename: image_pageXXXX.png.");
 977                  }
 978                  $pagenumber = (int)$matches[1];
 979  
 980                  if ($pagenumber == $index) {
 981                      $source = imagecreatefromstring($file->get_content());
 982                      $pagerotation = page_editor::get_page_rotation($grade->id, $index);
 983                      $degree = empty($pagerotation) ? 0 : $pagerotation->degree;
 984                      if ($rotateleft) {
 985                          $content = imagerotate($source, 90, 0);
 986                          $degree = ($degree + 90) % 360;
 987                      } else {
 988                          $content = imagerotate($source, -90, 0);
 989                          $degree = ($degree - 90) % 360;
 990                      }
 991                      $filename = $matches[0].'png';
 992                      $tmpdir = make_temp_directory(self::COMPONENT . '/' . self::PAGE_IMAGE_FILEAREA . '/'
 993                          . self::hash($assignment, $userid, $attemptnumber));
 994                      $tempfile = $tmpdir . '/' . time() . '_' . $filename;
 995                      imagepng($content, $tempfile);
 996  
 997                      $filearea = self::PAGE_IMAGE_FILEAREA;
 998                      $newfile = self::save_file($assignment, $userid, $attemptnumber, $filearea, $tempfile);
 999  
1000                      unlink($tempfile);
1001                      rmdir($tmpdir);
1002                      imagedestroy($source);
1003                      imagedestroy($content);
1004                      $file->delete();
1005                      if (!empty($newfile)) {
1006                          page_editor::set_page_rotation($grade->id, $pagenumber, true, $newfile->get_pathnamehash(), $degree);
1007                      }
1008                      return $newfile;
1009                  }
1010              }
1011          }
1012          return null;
1013      }
1014  
1015      /**
1016       * Convert jpg file to pdf file
1017       * @param int|\assign $assignment Assignment
1018       * @param int $userid User ID
1019       * @param int $attemptnumber Attempt Number
1020       * @param \stored_file $file file to save
1021       * @param null|array $size size of image
1022       * @return \stored_file
1023       * @throws \file_exception
1024       * @throws \stored_file_creation_exception
1025       */
1026      private static function save_jpg_to_pdf($assignment, $userid, $attemptnumber, $file, $size=null) {
1027          // Temporary file.
1028          $filename = $file->get_filename();
1029          $tmpdir = make_temp_directory('assignfeedback_editpdf' . DIRECTORY_SEPARATOR
1030              . self::TMP_JPG_TO_PDF_FILEAREA . DIRECTORY_SEPARATOR
1031              . self::hash($assignment, $userid, $attemptnumber));
1032          $tempfile = $tmpdir . DIRECTORY_SEPARATOR . $filename . ".pdf";
1033          // Determine orientation.
1034          $orientation = 'P';
1035          if (!empty($size['width']) && !empty($size['height'])) {
1036              if ($size['width'] > $size['height']) {
1037                  $orientation = 'L';
1038              }
1039          }
1040          // Save JPG image to PDF file.
1041          $pdf = new pdf();
1042          $pdf->SetHeaderMargin(0);
1043          $pdf->SetFooterMargin(0);
1044          $pdf->SetMargins(0, 0, 0, true);
1045          $pdf->setPrintFooter(false);
1046          $pdf->setPrintHeader(false);
1047          $pdf->setImageScale(PDF_IMAGE_SCALE_RATIO);
1048          $pdf->AddPage($orientation);
1049          $pdf->SetAutoPageBreak(false);
1050          // Width has to be define here to fit into A4 page. Otherwise the image will be inserted with original size.
1051          if ($orientation == 'P') {
1052              $pdf->Image('@' . $file->get_content(), 0, 0, 210);
1053          } else {
1054              $pdf->Image('@' . $file->get_content(), 0, 0, 297);
1055          }
1056          $pdf->setPageMark();
1057          $pdf->save_pdf($tempfile);
1058          $filearea = self::TMP_JPG_TO_PDF_FILEAREA;
1059          $pdffile = self::save_file($assignment, $userid, $attemptnumber, $filearea, $tempfile);
1060          if (file_exists($tempfile)) {
1061              unlink($tempfile);
1062              rmdir($tmpdir);
1063          }
1064          return $pdffile;
1065      }
1066  
1067      /**
1068       * Save rotated image data to file.
1069       * @param int|\assign $assignment Assignment
1070       * @param int $userid User ID
1071       * @param int $attemptnumber Attempt Number
1072       * @param resource $rotateddata image data to save
1073       * @param string $filename name of the image file
1074       * @return \stored_file
1075       * @throws \file_exception
1076       * @throws \stored_file_creation_exception
1077       */
1078      private static function save_rotated_image_file($assignment, $userid, $attemptnumber, $rotateddata, $filename) {
1079          $filearea = self::TMP_ROTATED_JPG_FILEAREA;
1080          $tmpdir = make_temp_directory('assignfeedback_editpdf' . DIRECTORY_SEPARATOR
1081              . $filearea . DIRECTORY_SEPARATOR
1082              . self::hash($assignment, $userid, $attemptnumber));
1083          $tempfile = $tmpdir . DIRECTORY_SEPARATOR . basename($filename);
1084          imagejpeg($rotateddata, $tempfile);
1085          $newfile = self::save_file($assignment, $userid, $attemptnumber, $filearea, $tempfile);
1086          if (file_exists($tempfile)) {
1087              unlink($tempfile);
1088              rmdir($tmpdir);
1089          }
1090          return $newfile;
1091      }
1092  
1093  }