[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/mod/assign/feedback/editpdf/classes/ -> document_services.php (source)

   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      /** File area for generated pdf */
  42      const FINAL_PDF_FILEAREA = 'download';
  43      /** File area for combined pdf */
  44      const COMBINED_PDF_FILEAREA = 'combined';
  45      /** File area for importing html */
  46      const IMPORT_HTML_FILEAREA = 'importhtml';
  47      /** File area for page images */
  48      const PAGE_IMAGE_FILEAREA = 'pages';
  49      /** File area for readonly page images */
  50      const PAGE_IMAGE_READONLY_FILEAREA = 'readonlypages';
  51      /** File area for the stamps */
  52      const STAMPS_FILEAREA = 'stamps';
  53      /** Filename for combined pdf */
  54      const COMBINED_PDF_FILENAME = 'combined.pdf';
  55  
  56      /** Base64 encoded blank pdf. This is the most reliable/fastest way to generate a blank pdf. */
  57      const BLANK_PDF_BASE64 = <<<EOD
  58  JVBERi0xLjQKJcOkw7zDtsOfCjIgMCBvYmoKPDwvTGVuZ3RoIDMgMCBSL0ZpbHRlci9GbGF0ZURl
  59  Y29kZT4+CnN0cmVhbQp4nDPQM1Qo5ypUMFAwALJMLU31jBQsTAz1LBSKUrnCtRTyuAIVAIcdB3IK
  60  ZW5kc3RyZWFtCmVuZG9iagoKMyAwIG9iago0MgplbmRvYmoKCjUgMCBvYmoKPDwKPj4KZW5kb2Jq
  61  Cgo2IDAgb2JqCjw8L0ZvbnQgNSAwIFIKL1Byb2NTZXRbL1BERi9UZXh0XQo+PgplbmRvYmoKCjEg
  62  MCBvYmoKPDwvVHlwZS9QYWdlL1BhcmVudCA0IDAgUi9SZXNvdXJjZXMgNiAwIFIvTWVkaWFCb3hb
  63  MCAwIDU5NSA4NDJdL0dyb3VwPDwvUy9UcmFuc3BhcmVuY3kvQ1MvRGV2aWNlUkdCL0kgdHJ1ZT4+
  64  L0NvbnRlbnRzIDIgMCBSPj4KZW5kb2JqCgo0IDAgb2JqCjw8L1R5cGUvUGFnZXMKL1Jlc291cmNl
  65  cyA2IDAgUgovTWVkaWFCb3hbIDAgMCA1OTUgODQyIF0KL0tpZHNbIDEgMCBSIF0KL0NvdW50IDE+
  66  PgplbmRvYmoKCjcgMCBvYmoKPDwvVHlwZS9DYXRhbG9nL1BhZ2VzIDQgMCBSCi9PcGVuQWN0aW9u
  67  WzEgMCBSIC9YWVogbnVsbCBudWxsIDBdCi9MYW5nKGVuLUFVKQo+PgplbmRvYmoKCjggMCBvYmoK
  68  PDwvQ3JlYXRvcjxGRUZGMDA1NzAwNzIwMDY5MDA3NDAwNjUwMDcyPgovUHJvZHVjZXI8RkVGRjAw
  69  NEMwMDY5MDA2MjAwNzIwMDY1MDA0RjAwNjYwMDY2MDA2OTAwNjMwMDY1MDAyMDAwMzQwMDJFMDAz
  70  ND4KL0NyZWF0aW9uRGF0ZShEOjIwMTYwMjI2MTMyMzE0KzA4JzAwJyk+PgplbmRvYmoKCnhyZWYK
  71  MCA5CjAwMDAwMDAwMDAgNjU1MzUgZiAKMDAwMDAwMDIyNiAwMDAwMCBuIAowMDAwMDAwMDE5IDAw
  72  MDAwIG4gCjAwMDAwMDAxMzIgMDAwMDAgbiAKMDAwMDAwMDM2OCAwMDAwMCBuIAowMDAwMDAwMTUx
  73  IDAwMDAwIG4gCjAwMDAwMDAxNzMgMDAwMDAgbiAKMDAwMDAwMDQ2NiAwMDAwMCBuIAowMDAwMDAw
  74  NTYyIDAwMDAwIG4gCnRyYWlsZXIKPDwvU2l6ZSA5L1Jvb3QgNyAwIFIKL0luZm8gOCAwIFIKL0lE
  75  IFsgPEJDN0REQUQwRDQyOTQ1OTQ2OUU4NzJCMjI1ODUyNkU4Pgo8QkM3RERBRDBENDI5NDU5NDY5
  76  RTg3MkIyMjU4NTI2RTg+IF0KL0RvY0NoZWNrc3VtIC9BNTYwMEZCMDAzRURCRTg0MTNBNTk3RTZF
  77  MURDQzJBRgo+PgpzdGFydHhyZWYKNzM2CiUlRU9GCg==
  78  EOD;
  79  
  80      /**
  81       * This function will take an int or an assignment instance and
  82       * return an assignment instance. It is just for convenience.
  83       * @param int|\assign $assignment
  84       * @return assign
  85       */
  86      private static function get_assignment_from_param($assignment) {
  87          global $CFG;
  88  
  89          require_once($CFG->dirroot . '/mod/assign/locallib.php');
  90  
  91          if (!is_object($assignment)) {
  92              $cm = \get_coursemodule_from_instance('assign', $assignment, 0, false, MUST_EXIST);
  93              $context = \context_module::instance($cm->id);
  94  
  95              $assignment = new \assign($context, null, null);
  96          }
  97          return $assignment;
  98      }
  99  
 100      /**
 101       * Get a hash that will be unique and can be used in a path name.
 102       * @param int|\assign $assignment
 103       * @param int $userid
 104       * @param int $attemptnumber (-1 means latest attempt)
 105       */
 106      private static function hash($assignment, $userid, $attemptnumber) {
 107          if (is_object($assignment)) {
 108              $assignmentid = $assignment->get_instance()->id;
 109          } else {
 110              $assignmentid = $assignment;
 111          }
 112          return sha1($assignmentid . '_' . $userid . '_' . $attemptnumber);
 113      }
 114  
 115      /**
 116       * Use a DOM parser to accurately replace images with their alt text.
 117       * @param string $html
 118       * @return string New html with no image tags.
 119       */
 120      protected static function strip_images($html) {
 121          $dom = new DOMDocument();
 122          $dom->loadHTML("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>" . $html);
 123          $images = $dom->getElementsByTagName('img');
 124          $i = 0;
 125  
 126          for ($i = ($images->length - 1); $i >= 0; $i--) {
 127              $node = $images->item($i);
 128  
 129              if ($node->hasAttribute('alt')) {
 130                  $replacement = ' [ ' . $node->getAttribute('alt') . ' ] ';
 131              } else {
 132                  $replacement = ' ';
 133              }
 134  
 135              $text = $dom->createTextNode($replacement);
 136              $node->parentNode->replaceChild($text, $node);
 137          }
 138          $count = 1;
 139          return str_replace("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>", "", $dom->saveHTML(), $count);
 140      }
 141  
 142      /**
 143       * This function will search for all files that can be converted
 144       * and concatinated into a PDF (1.4) - for any submission plugin
 145       * for this students attempt.
 146       * @param int|\assign $assignment
 147       * @param int $userid
 148       * @param int $attemptnumber (-1 means latest attempt)
 149       * @return array(stored_file)
 150       */
 151      public static function list_compatible_submission_files_for_attempt($assignment, $userid, $attemptnumber) {
 152          global $USER, $DB;
 153  
 154          $assignment = self::get_assignment_from_param($assignment);
 155  
 156          // Capability checks.
 157          if (!$assignment->can_view_submission($userid)) {
 158              \print_error('nopermission');
 159          }
 160  
 161          $files = array();
 162  
 163          if ($assignment->get_instance()->teamsubmission) {
 164              $submission = $assignment->get_group_submission($userid, 0, false);
 165          } else {
 166              $submission = $assignment->get_user_submission($userid, false);
 167          }
 168          $user = $DB->get_record('user', array('id' => $userid));
 169  
 170          // User has not submitted anything yet.
 171          if (!$submission) {
 172              return $files;
 173          }
 174  
 175          $fs = get_file_storage();
 176          // Ask each plugin for it's list of files.
 177          foreach ($assignment->get_submission_plugins() as $plugin) {
 178              if ($plugin->is_enabled() && $plugin->is_visible()) {
 179                  $pluginfiles = $plugin->get_files($submission, $user);
 180                  foreach ($pluginfiles as $filename => $file) {
 181                      if ($file instanceof \stored_file) {
 182                          if ($file->get_mimetype() === 'application/pdf') {
 183                              $files[$filename] = $file;
 184                          } else if ($convertedfile = $fs->get_converted_document($file, 'pdf')) {
 185                              $files[$filename] = $convertedfile;
 186                          }
 187                      } else {
 188                          // Create a tmp stored_file from this html string.
 189                          $file = reset($file);
 190                          // Strip image tags, because they will not be resolvable.
 191                          $file = self::strip_images($file);
 192                          $record = new \stdClass();
 193                          $record->contextid = $assignment->get_context()->id;
 194                          $record->component = 'assignfeedback_editpdf';
 195                          $record->filearea = self::IMPORT_HTML_FILEAREA;
 196                          $record->itemid = $submission->id;
 197                          $record->filepath = '/';
 198                          $record->filename = $plugin->get_type() . '-' . $filename;
 199  
 200                          $htmlfile = $fs->create_file_from_string($record, $file);
 201                          $convertedfile = $fs->get_converted_document($htmlfile, 'pdf');
 202                          $htmlfile->delete();
 203                          if ($convertedfile) {
 204                              $files[$filename] = $convertedfile;
 205                          }
 206                      }
 207                  }
 208              }
 209          }
 210          return $files;
 211      }
 212  
 213      /**
 214       * This function return the combined pdf for all valid submission files.
 215       * @param int|\assign $assignment
 216       * @param int $userid
 217       * @param int $attemptnumber (-1 means latest attempt)
 218       * @return stored_file
 219       */
 220      public static function get_combined_pdf_for_attempt($assignment, $userid, $attemptnumber) {
 221  
 222          global $USER, $DB;
 223  
 224          $assignment = self::get_assignment_from_param($assignment);
 225  
 226          // Capability checks.
 227          if (!$assignment->can_view_submission($userid)) {
 228              \print_error('nopermission');
 229          }
 230  
 231          $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
 232          if ($assignment->get_instance()->teamsubmission) {
 233              $submission = $assignment->get_group_submission($userid, 0, false);
 234          } else {
 235              $submission = $assignment->get_user_submission($userid, false);
 236          }
 237  
 238          $contextid = $assignment->get_context()->id;
 239          $component = 'assignfeedback_editpdf';
 240          $filearea = self::COMBINED_PDF_FILEAREA;
 241          $itemid = $grade->id;
 242          $filepath = '/';
 243          $filename = self::COMBINED_PDF_FILENAME;
 244          $fs = \get_file_storage();
 245  
 246          $combinedpdf = $fs->get_file($contextid, $component, $filearea, $itemid, $filepath, $filename);
 247          if (!$combinedpdf ||
 248                  ($submission && ($combinedpdf->get_timemodified() < $submission->timemodified))) {
 249              return self::generate_combined_pdf_for_attempt($assignment, $userid, $attemptnumber);
 250          }
 251          return $combinedpdf;
 252      }
 253  
 254      /**
 255       * This function will take all of the compatible files for a submission
 256       * and combine them into one PDF.
 257       * @param int|\assign $assignment
 258       * @param int $userid
 259       * @param int $attemptnumber (-1 means latest attempt)
 260       * @return stored_file
 261       */
 262      public static function generate_combined_pdf_for_attempt($assignment, $userid, $attemptnumber) {
 263          global $CFG;
 264  
 265          require_once($CFG->libdir . '/pdflib.php');
 266  
 267          $assignment = self::get_assignment_from_param($assignment);
 268  
 269          if (!$assignment->can_view_submission($userid)) {
 270              \print_error('nopermission');
 271          }
 272  
 273          $files = self::list_compatible_submission_files_for_attempt($assignment, $userid, $attemptnumber);
 274  
 275          $pdf = new pdf();
 276          if ($files) {
 277              // Create a mega joined PDF.
 278              $compatiblepdfs = array();
 279              foreach ($files as $file) {
 280                  $compatiblepdf = pdf::ensure_pdf_compatible($file);
 281                  if ($compatiblepdf) {
 282                      array_push($compatiblepdfs, $compatiblepdf);
 283                  }
 284              }
 285  
 286              $tmpdir = \make_temp_directory('assignfeedback_editpdf/combined/' . self::hash($assignment, $userid, $attemptnumber));
 287              $tmpfile = $tmpdir . '/' . self::COMBINED_PDF_FILENAME;
 288  
 289              @unlink($tmpfile);
 290              try {
 291                  $pagecount = $pdf->combine_pdfs($compatiblepdfs, $tmpfile);
 292              } catch (\Exception $e) {
 293                  debugging('TCPDF could not process the pdf files:' . $e->getMessage(), DEBUG_DEVELOPER);
 294                  // TCPDF does not recover from errors so we need to re-initialise the class.
 295                  $pagecount = 0;
 296              }
 297              if ($pagecount == 0) {
 298                  // We at least want a single blank page.
 299                  debugging('TCPDF did not produce a valid pdf:' . $tmpfile . '. Replacing with a blank pdf.', DEBUG_DEVELOPER);
 300                  @unlink($tmpfile);
 301                  $files = false;
 302              }
 303          }
 304          $pdf->Close(); // No real need to close this pdf, because it has been saved by combine_pdfs(), but for clarity.
 305  
 306          $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
 307          $record = new \stdClass();
 308  
 309          $record->contextid = $assignment->get_context()->id;
 310          $record->component = 'assignfeedback_editpdf';
 311          $record->filearea = self::COMBINED_PDF_FILEAREA;
 312          $record->itemid = $grade->id;
 313          $record->filepath = '/';
 314          $record->filename = self::COMBINED_PDF_FILENAME;
 315          $fs = \get_file_storage();
 316  
 317          $fs->delete_area_files($record->contextid, $record->component, $record->filearea, $record->itemid);
 318  
 319          // Detect corrupt generated pdfs and replace with a blank one.
 320          if ($files) {
 321              $verifypdf = new pdf();
 322              $pagecount = $verifypdf->load_pdf($tmpfile);
 323              if ($pagecount <= 0) {
 324                  $files = false;
 325              }
 326              $verifypdf->Close(); // PDF loaded and never saved/outputted needs to be closed.
 327          }
 328  
 329          if (!$files) {
 330              $file = $fs->create_file_from_string($record, base64_decode(self::BLANK_PDF_BASE64));
 331          } else {
 332              // This was a combined pdf.
 333              $file = $fs->create_file_from_pathname($record, $tmpfile);
 334              @unlink($tmpfile);
 335  
 336              // Test the generated file for correctness.
 337              $compatiblepdf = pdf::ensure_pdf_compatible($file);
 338          }
 339  
 340          return $file;
 341      }
 342  
 343      /**
 344       * This function will return the number of pages of a pdf.
 345       * @param int|\assign $assignment
 346       * @param int $userid
 347       * @param int $attemptnumber (-1 means latest attempt)
 348       * @param bool $readonly When true we get the number of pages for the readonly version.
 349       * @return int number of pages
 350       */
 351      public static function page_number_for_attempt($assignment, $userid, $attemptnumber, $readonly = false) {
 352          global $CFG;
 353  
 354          require_once($CFG->libdir . '/pdflib.php');
 355  
 356          $assignment = self::get_assignment_from_param($assignment);
 357  
 358          if (!$assignment->can_view_submission($userid)) {
 359              \print_error('nopermission');
 360          }
 361  
 362          // When in readonly we can return the number of images in the DB because they should already exist,
 363          // if for some reason they do not, then we proceed as for the normal version.
 364          if ($readonly) {
 365              $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
 366              $fs = get_file_storage();
 367              $files = $fs->get_directory_files($assignment->get_context()->id, 'assignfeedback_editpdf',
 368                  self::PAGE_IMAGE_READONLY_FILEAREA, $grade->id, '/');
 369              $pagecount = count($files);
 370              if ($pagecount > 0) {
 371                  return $pagecount;
 372              }
 373          }
 374  
 375          // Get a combined pdf file from all submitted pdf files.
 376          $file = self::get_combined_pdf_for_attempt($assignment, $userid, $attemptnumber);
 377          if (!$file) {
 378              \print_error('Could not generate combined pdf.');
 379          }
 380  
 381          // Store the combined pdf file somewhere to be opened by tcpdf.
 382          $tmpdir = \make_temp_directory('assignfeedback_editpdf/pagetotal/'
 383              . self::hash($assignment, $userid, $attemptnumber));
 384          $combined = $tmpdir . '/' . self::COMBINED_PDF_FILENAME;
 385          $file->copy_content_to($combined); // Copy the file.
 386  
 387          // Get the total number of pages.
 388          $pdf = new pdf();
 389          $pagecount = $pdf->set_pdf($combined);
 390          $pdf->Close(); // PDF loaded and never saved/outputted needs to be closed.
 391  
 392          // Delete temporary folders and files.
 393          @unlink($combined);
 394          @rmdir($tmpdir);
 395  
 396          return $pagecount;
 397      }
 398  
 399      /**
 400       * This function will generate and return a list of the page images from a pdf.
 401       * @param int|\assign $assignment
 402       * @param int $userid
 403       * @param int $attemptnumber (-1 means latest attempt)
 404       * @return array(stored_file)
 405       */
 406      public static function generate_page_images_for_attempt($assignment, $userid, $attemptnumber) {
 407          global $CFG;
 408  
 409          require_once($CFG->libdir . '/pdflib.php');
 410  
 411          $assignment = self::get_assignment_from_param($assignment);
 412  
 413          if (!$assignment->can_view_submission($userid)) {
 414              \print_error('nopermission');
 415          }
 416  
 417          // Need to generate the page images - first get a combined pdf.
 418          $file = self::get_combined_pdf_for_attempt($assignment, $userid, $attemptnumber);
 419          if (!$file) {
 420              throw new \moodle_exception('Could not generate combined pdf.');
 421          }
 422  
 423          $tmpdir = \make_temp_directory('assignfeedback_editpdf/pageimages/' . self::hash($assignment, $userid, $attemptnumber));
 424          $combined = $tmpdir . '/' . self::COMBINED_PDF_FILENAME;
 425          $file->copy_content_to($combined); // Copy the file.
 426  
 427          $pdf = new pdf();
 428  
 429          $pdf->set_image_folder($tmpdir);
 430          $pagecount = $pdf->set_pdf($combined);
 431  
 432          $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
 433  
 434          $record = new \stdClass();
 435          $record->contextid = $assignment->get_context()->id;
 436          $record->component = 'assignfeedback_editpdf';
 437          $record->filearea = self::PAGE_IMAGE_FILEAREA;
 438          $record->itemid = $grade->id;
 439          $record->filepath = '/';
 440          $fs = \get_file_storage();
 441  
 442          // Remove the existing content of the filearea.
 443          $fs->delete_area_files($record->contextid, $record->component, $record->filearea, $record->itemid);
 444  
 445          $files = array();
 446          for ($i = 0; $i < $pagecount; $i++) {
 447              $image = $pdf->get_image($i);
 448              $record->filename = basename($image);
 449              $files[$i] = $fs->create_file_from_pathname($record, $tmpdir . '/' . $image);
 450              @unlink($tmpdir . '/' . $image);
 451          }
 452          $pdf->Close(); // PDF loaded and never saved/outputted needs to be closed.
 453  
 454          @unlink($combined);
 455          @rmdir($tmpdir);
 456  
 457          return $files;
 458      }
 459  
 460      /**
 461       * This function returns a list of the page images from a pdf.
 462       *
 463       * The readonly version is different than the normal one. The readonly version contains a copy
 464       * of the pages in the state they were when the PDF was annotated, by doing so we prevent the
 465       * the pages that are displayed to change as soon as the submission changes.
 466       *
 467       * Though there is an edge case, if the PDF was annotated before MDL-45580, then it is possible
 468       * that we do not find any readonly version of the pages. In that case, we will get the normal
 469       * pages and copy them to the readonly area. This ensures that the pages will remain in that
 470       * state until the submission is updated. When the normal files do not exist, we throw an exception
 471       * because the readonly pages should only ever be displayed after a teacher has annotated the PDF,
 472       * they would not exist until they do.
 473       *
 474       * @param int|\assign $assignment
 475       * @param int $userid
 476       * @param int $attemptnumber (-1 means latest attempt)
 477       * @param bool $readonly If true, then we are requesting the readonly version.
 478       * @return array(stored_file)
 479       */
 480      public static function get_page_images_for_attempt($assignment, $userid, $attemptnumber, $readonly = false) {
 481  
 482          $assignment = self::get_assignment_from_param($assignment);
 483  
 484          if (!$assignment->can_view_submission($userid)) {
 485              \print_error('nopermission');
 486          }
 487  
 488          if ($assignment->get_instance()->teamsubmission) {
 489              $submission = $assignment->get_group_submission($userid, 0, false);
 490          } else {
 491              $submission = $assignment->get_user_submission($userid, false);
 492          }
 493          $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
 494  
 495          $contextid = $assignment->get_context()->id;
 496          $component = 'assignfeedback_editpdf';
 497          $itemid = $grade->id;
 498          $filepath = '/';
 499          $filearea = self::PAGE_IMAGE_FILEAREA;
 500  
 501          $fs = \get_file_storage();
 502  
 503          // If we are after the readonly pages...
 504          if ($readonly) {
 505              $filearea = self::PAGE_IMAGE_READONLY_FILEAREA;
 506              if ($fs->is_area_empty($contextid, $component, $filearea, $itemid)) {
 507                  // We have a problem here, we were supposed to find the files.
 508                  // Attempt to re-generate the pages from the combined images.
 509                  self::generate_page_images_for_attempt($assignment, $userid, $attemptnumber);
 510                  self::copy_pages_to_readonly_area($assignment, $grade);
 511              }
 512          }
 513  
 514          $files = $fs->get_directory_files($contextid, $component, $filearea, $itemid, $filepath);
 515  
 516          $pages = array();
 517          if (!empty($files)) {
 518              $first = reset($files);
 519              if (!$readonly && $first->get_timemodified() < $submission->timemodified) {
 520                  // Image files are stale, we need to regenerate them, except in readonly mode.
 521                  // We also need to remove the draft annotations and comments associated with this attempt.
 522                  $fs->delete_area_files($contextid, $component, $filearea, $itemid);
 523                  page_editor::delete_draft_content($itemid);
 524                  $files = array();
 525              } else {
 526  
 527                  // Need to reorder the files following their name.
 528                  // because get_directory_files() return a different order than generate_page_images_for_attempt().
 529                  foreach($files as $file) {
 530                      // Extract the page number from the file name image_pageXXXX.png.
 531                      preg_match('/page([\d]+)\./', $file->get_filename(), $matches);
 532                      if (empty($matches) or !is_numeric($matches[1])) {
 533                          throw new \coding_exception("'" . $file->get_filename()
 534                              . "' file hasn't the expected format filename: image_pageXXXX.png.");
 535                      }
 536                      $pagenumber = (int)$matches[1];
 537  
 538                      // Save the page in the ordered array.
 539                      $pages[$pagenumber] = $file;
 540                  }
 541                  ksort($pages);
 542              }
 543          }
 544  
 545          if (empty($pages)) {
 546              if ($readonly) {
 547                  // This should never happen, there should be a version of the pages available
 548                  // whenever we are requesting the readonly version.
 549                  throw new \moodle_exception('Could not find readonly pages for grade ' . $grade->id);
 550              }
 551              $pages = self::generate_page_images_for_attempt($assignment, $userid, $attemptnumber);
 552          }
 553  
 554          return $pages;
 555      }
 556  
 557      /**
 558       * This function returns sensible filename for a feedback file.
 559       * @param int|\assign $assignment
 560       * @param int $userid
 561       * @param int $attemptnumber (-1 means latest attempt)
 562       * @return string
 563       */
 564      protected static function get_downloadable_feedback_filename($assignment, $userid, $attemptnumber) {
 565          global $DB;
 566  
 567          $assignment = self::get_assignment_from_param($assignment);
 568  
 569          $groupmode = groups_get_activity_groupmode($assignment->get_course_module());
 570          $groupname = '';
 571          if ($groupmode) {
 572              $groupid = groups_get_activity_group($assignment->get_course_module(), true);
 573              $groupname = groups_get_group_name($groupid).'-';
 574          }
 575          if ($groupname == '-') {
 576              $groupname = '';
 577          }
 578          $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
 579          $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
 580  
 581          if ($assignment->is_blind_marking()) {
 582              $prefix = $groupname . get_string('participant', 'assign');
 583              $prefix = str_replace('_', ' ', $prefix);
 584              $prefix = clean_filename($prefix . '_' . $assignment->get_uniqueid_for_user($userid) . '_');
 585          } else {
 586              $prefix = $groupname . fullname($user);
 587              $prefix = str_replace('_', ' ', $prefix);
 588              $prefix = clean_filename($prefix . '_' . $assignment->get_uniqueid_for_user($userid) . '_');
 589          }
 590          $prefix .= $grade->attemptnumber;
 591  
 592          return $prefix . '.pdf';
 593      }
 594  
 595      /**
 596       * This function takes the combined pdf and embeds all the comments and annotations.
 597       *
 598       * This also moves the annotations and comments from drafts to not drafts. And it will
 599       * copy all the images stored to the readonly area, so that they can be viewed online, and
 600       * not be overwritten when a new submission is sent.
 601       *
 602       * @param int|\assign $assignment
 603       * @param int $userid
 604       * @param int $attemptnumber (-1 means latest attempt)
 605       * @return stored_file
 606       */
 607      public static function generate_feedback_document($assignment, $userid, $attemptnumber) {
 608  
 609          $assignment = self::get_assignment_from_param($assignment);
 610  
 611          if (!$assignment->can_view_submission($userid)) {
 612              \print_error('nopermission');
 613          }
 614          if (!$assignment->can_grade()) {
 615              \print_error('nopermission');
 616          }
 617  
 618          // Need to generate the page images - first get a combined pdf.
 619          $file = self::get_combined_pdf_for_attempt($assignment, $userid, $attemptnumber);
 620          if (!$file) {
 621              throw new \moodle_exception('Could not generate combined pdf.');
 622          }
 623  
 624          $tmpdir = \make_temp_directory('assignfeedback_editpdf/final/' . self::hash($assignment, $userid, $attemptnumber));
 625          $combined = $tmpdir . '/' . self::COMBINED_PDF_FILENAME;
 626          $file->copy_content_to($combined); // Copy the file.
 627  
 628          $pdf = new pdf();
 629  
 630          $fs = \get_file_storage();
 631          $stamptmpdir = \make_temp_directory('assignfeedback_editpdf/stamps/' . self::hash($assignment, $userid, $attemptnumber));
 632          $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
 633          // Copy any new stamps to this instance.
 634          if ($files = $fs->get_area_files($assignment->get_context()->id,
 635                                           'assignfeedback_editpdf',
 636                                           'stamps',
 637                                           $grade->id,
 638                                           "filename",
 639                                           false)) {
 640              foreach ($files as $file) {
 641                  $filename = $stamptmpdir . '/' . $file->get_filename();
 642                  $file->copy_content_to($filename); // Copy the file.
 643              }
 644          }
 645  
 646          $pagecount = $pdf->set_pdf($combined);
 647          $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
 648          page_editor::release_drafts($grade->id);
 649  
 650          for ($i = 0; $i < $pagecount; $i++) {
 651              $pdf->copy_page();
 652              $comments = page_editor::get_comments($grade->id, $i, false);
 653              $annotations = page_editor::get_annotations($grade->id, $i, false);
 654  
 655              foreach ($comments as $comment) {
 656                  $pdf->add_comment($comment->rawtext,
 657                                    $comment->x,
 658                                    $comment->y,
 659                                    $comment->width,
 660                                    $comment->colour);
 661              }
 662  
 663              foreach ($annotations as $annotation) {
 664                  $pdf->add_annotation($annotation->x,
 665                                       $annotation->y,
 666                                       $annotation->endx,
 667                                       $annotation->endy,
 668                                       $annotation->colour,
 669                                       $annotation->type,
 670                                       $annotation->path,
 671                                       $stamptmpdir);
 672              }
 673          }
 674  
 675          fulldelete($stamptmpdir);
 676  
 677          $filename = self::get_downloadable_feedback_filename($assignment, $userid, $attemptnumber);
 678          $filename = clean_param($filename, PARAM_FILE);
 679  
 680          $generatedpdf = $tmpdir . '/' . $filename;
 681          $pdf->save_pdf($generatedpdf);
 682  
 683  
 684          $record = new \stdClass();
 685  
 686          $record->contextid = $assignment->get_context()->id;
 687          $record->component = 'assignfeedback_editpdf';
 688          $record->filearea = self::FINAL_PDF_FILEAREA;
 689          $record->itemid = $grade->id;
 690          $record->filepath = '/';
 691          $record->filename = $filename;
 692  
 693  
 694          // Only keep one current version of the generated pdf.
 695          $fs->delete_area_files($record->contextid, $record->component, $record->filearea, $record->itemid);
 696  
 697          $file = $fs->create_file_from_pathname($record, $generatedpdf);
 698  
 699          // Cleanup.
 700          @unlink($generatedpdf);
 701          @unlink($combined);
 702          @rmdir($tmpdir);
 703  
 704          self::copy_pages_to_readonly_area($assignment, $grade);
 705  
 706          return $file;
 707      }
 708  
 709      /**
 710       * Copy the pages image to the readonly area.
 711       *
 712       * @param int|\assign $assignment The assignment.
 713       * @param \stdClass $grade The grade record.
 714       * @return void
 715       */
 716      public static function copy_pages_to_readonly_area($assignment, $grade) {
 717          $fs = get_file_storage();
 718          $assignment = self::get_assignment_from_param($assignment);
 719          $contextid = $assignment->get_context()->id;
 720          $component = 'assignfeedback_editpdf';
 721          $itemid = $grade->id;
 722  
 723          // Get all the pages.
 724          $originalfiles = $fs->get_area_files($contextid, $component, self::PAGE_IMAGE_FILEAREA, $itemid);
 725          if (empty($originalfiles)) {
 726              // Nothing to do here...
 727              return;
 728          }
 729  
 730          // Delete the old readonly files.
 731          $fs->delete_area_files($contextid, $component, self::PAGE_IMAGE_READONLY_FILEAREA, $itemid);
 732  
 733          // Do the copying.
 734          foreach ($originalfiles as $originalfile) {
 735              $fs->create_file_from_storedfile(array('filearea' => self::PAGE_IMAGE_READONLY_FILEAREA), $originalfile);
 736          }
 737      }
 738  
 739      /**
 740       * This function returns the generated pdf (if it exists).
 741       * @param int|\assign $assignment
 742       * @param int $userid
 743       * @param int $attemptnumber (-1 means latest attempt)
 744       * @return stored_file
 745       */
 746      public static function get_feedback_document($assignment, $userid, $attemptnumber) {
 747  
 748          $assignment = self::get_assignment_from_param($assignment);
 749  
 750          if (!$assignment->can_view_submission($userid)) {
 751              \print_error('nopermission');
 752          }
 753  
 754          $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
 755  
 756          $contextid = $assignment->get_context()->id;
 757          $component = 'assignfeedback_editpdf';
 758          $filearea = self::FINAL_PDF_FILEAREA;
 759          $itemid = $grade->id;
 760          $filepath = '/';
 761  
 762          $fs = \get_file_storage();
 763          $files = $fs->get_area_files($contextid,
 764                                       $component,
 765                                       $filearea,
 766                                       $itemid,
 767                                       "itemid, filepath, filename",
 768                                       false);
 769          if ($files) {
 770              return reset($files);
 771          }
 772          return false;
 773      }
 774  
 775      /**
 776       * This function deletes the generated pdf for a student.
 777       * @param int|\assign $assignment
 778       * @param int $userid
 779       * @param int $attemptnumber (-1 means latest attempt)
 780       * @return bool
 781       */
 782      public static function delete_feedback_document($assignment, $userid, $attemptnumber) {
 783  
 784          $assignment = self::get_assignment_from_param($assignment);
 785  
 786          if (!$assignment->can_view_submission($userid)) {
 787              \print_error('nopermission');
 788          }
 789          if (!$assignment->can_grade()) {
 790              \print_error('nopermission');
 791          }
 792  
 793          $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
 794  
 795          $contextid = $assignment->get_context()->id;
 796          $component = 'assignfeedback_editpdf';
 797          $filearea = self::FINAL_PDF_FILEAREA;
 798          $itemid = $grade->id;
 799  
 800          $fs = \get_file_storage();
 801          return $fs->delete_area_files($contextid, $component, $filearea, $itemid);
 802      }
 803  
 804  }


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1