Merge branch 'wip-mdl-42222' of git://github.com/rajeshtaneja/moodle
[moodle.git] / mod / assign / feedback / editpdf / classes / document_services.php
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/>.
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  */
25 namespace assignfeedback_editpdf;
27 /**
28  * Functions for generating the annotated pdf.
29  *
30  * This class controls the ingest of student submission files to a normalised
31  * PDF 1.4 document with all submission files concatinated together. It also
32  * provides the functions to generate a downloadable pdf with all comments and
33  * annotations embedded.
34  * @copyright 2012 Davo Smith
35  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
36  */
37 class document_services {
39     /** File area for generated pdf */
40     const FINAL_PDF_FILEAREA = 'download';
41     /** File area for combined pdf */
42     const COMBINED_PDF_FILEAREA = 'combined';
43     /** File area for page images */
44     const PAGE_IMAGE_FILEAREA = 'pages';
45     /** Filename for combined pdf */
46     const COMBINED_PDF_FILENAME = 'combined.pdf';
48     /**
49      * This function will take an int or an assignment instance and
50      * return an assignment instance. It is just for convenience.
51      * @param int|\assign $assignment
52      * @return assign
53      */
54     private static function get_assignment_from_param($assignment) {
55         global $CFG;
57         require_once($CFG->dirroot . '/mod/assign/locallib.php');
59         if (!is_object($assignment)) {
60             $cm = \get_coursemodule_from_instance('assign', $assignment, 0, false, MUST_EXIST);
61             $context = \context_module::instance($cm->id);
63             $assignment = new \assign($context, null, null);
64         }
65         return $assignment;
66     }
68     /**
69      * Get a hash that will be unique and can be used in a path name.
70      * @param int|\assign $assignment
71      * @param int $userid
72      * @param int $attemptnumber (-1 means latest attempt)
73      */
74     private static function hash($assignment, $userid, $attemptnumber) {
75         if (is_object($assignment)) {
76             $assignmentid = $assignment->get_instance()->id;
77         } else {
78             $assignmentid = $assignment;
79         }
80         return sha1($assignmentid . '_' . $userid . '_' . $attemptnumber);
81     }
83     /**
84      * This function will search for all files that can be converted
85      * and concatinated into a PDF (1.4) - for any submission plugin
86      * for this students attempt.
87      * @param int|\assign $assignment
88      * @param int $userid
89      * @param int $attemptnumber (-1 means latest attempt)
90      * @return array(stored_file)
91      */
92     public static function list_compatible_submission_files_for_attempt($assignment, $userid, $attemptnumber) {
93         global $USER, $DB;
95         $assignment = self::get_assignment_from_param($assignment);
97         // Capability checks.
98         if (!$assignment->can_view_submission($userid)) {
99             \print_error('nopermission');
100         }
102         $files = array();
104         if ($assignment->get_instance()->teamsubmission) {
105             $submission = $assignment->get_group_submission($userid, 0, false);
106         } else {
107             $submission = $assignment->get_user_submission($userid, false);
108         }
109         $user = $DB->get_record('user', array('id' => $userid));
111         // User has not submitted anything yet.
112         if (!$submission) {
113             return $files;
114         }
115         // Ask each plugin for it's list of files.
116         foreach ($assignment->get_submission_plugins() as $plugin) {
117             if ($plugin->is_enabled() && $plugin->is_visible()) {
118                 $pluginfiles = $plugin->get_files($submission, $user);
119                 foreach ($pluginfiles as $filename => $file) {
120                     if (($file instanceof \stored_file) && ($file->get_mimetype() === 'application/pdf')) {
121                         $files[$filename] = $file;
122                     }
123                 }
124             }
125         }
126         return $files;
127     }
129     /**
130      * This function return the combined pdf for all valid submission files.
131      * @param int|\assign $assignment
132      * @param int $userid
133      * @param int $attemptnumber (-1 means latest attempt)
134      * @return stored_file
135      */
136     public static function get_combined_pdf_for_attempt($assignment, $userid, $attemptnumber) {
138         global $USER, $DB;
140         $assignment = self::get_assignment_from_param($assignment);
142         // Capability checks.
143         if (!$assignment->can_view_submission($userid)) {
144             \print_error('nopermission');
145         }
147         $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
148         if ($assignment->get_instance()->teamsubmission) {
149             $submission = $assignment->get_group_submission($userid, 0, false);
150         } else {
151             $submission = $assignment->get_user_submission($userid, false);
152         }
154         $contextid = $assignment->get_context()->id;
155         $component = 'assignfeedback_editpdf';
156         $filearea = self::COMBINED_PDF_FILEAREA;
157         $itemid = $grade->id;
158         $filepath = '/';
159         $filename = self::COMBINED_PDF_FILENAME;
160         $fs = \get_file_storage();
162         $combinedpdf = $fs->get_file($contextid, $component, $filearea, $itemid, $filepath, $filename);
163         if (!$combinedpdf ||
164                 ($submission && ($combinedpdf->get_timemodified() < $submission->timemodified))) {
165             return self::generate_combined_pdf_for_attempt($assignment, $userid, $attemptnumber);
166         }
167         return $combinedpdf;
168     }
170     /**
171      * This function will take all of the compatible files for a submission
172      * and combine them into one PDF.
173      * @param int|\assign $assignment
174      * @param int $userid
175      * @param int $attemptnumber (-1 means latest attempt)
176      * @return stored_file
177      */
178     public static function generate_combined_pdf_for_attempt($assignment, $userid, $attemptnumber) {
179         global $CFG;
181         require_once($CFG->libdir . '/pdflib.php');
183         $assignment = self::get_assignment_from_param($assignment);
185         if (!$assignment->can_view_submission($userid)) {
186             \print_error('nopermission');
187         }
189         $files = self::list_compatible_submission_files_for_attempt($assignment, $userid, $attemptnumber);
191         $pdf = new pdf();
192         if (!$files) {
193             // No valid submission files - create an empty pdf.
194             $pdf->AddPage();
195         } else {
197             // Create a mega joined PDF.
198             $compatiblepdfs = array();
199             foreach ($files as $file) {
200                 $compatiblepdf = pdf::ensure_pdf_compatible($file);
201                 if ($compatiblepdf) {
202                     array_push($compatiblepdfs, $compatiblepdf);
203                 }
204             }
206             $tmpdir = \make_temp_directory('assignfeedback_editpdf/combined/' . self::hash($assignment, $userid, $attemptnumber));
207             $tmpfile = $tmpdir . '/' . self::COMBINED_PDF_FILENAME;
209             @unlink($tmpfile);
210             $pagecount = $pdf->combine_pdfs($compatiblepdfs, $tmpfile);
211             if ($pagecount == 0) {
212                 // We at least want a single blank page.
213                 $pdf->AddPage();
214                 @unlink($tmpfile);
215                 $files = false;
216             }
217         }
219         $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
220         $record = new \stdClass();
222         $record->contextid = $assignment->get_context()->id;
223         $record->component = 'assignfeedback_editpdf';
224         $record->filearea = self::COMBINED_PDF_FILEAREA;
225         $record->itemid = $grade->id;
226         $record->filepath = '/';
227         $record->filename = self::COMBINED_PDF_FILENAME;
228         $fs = \get_file_storage();
230         $fs->delete_area_files($record->contextid, $record->component, $record->filearea, $record->itemid);
232         if (!$files) {
233             // This was a blank pdf.
234             $content = $pdf->Output(self::COMBINED_PDF_FILENAME, 'S');
235             $file = $fs->create_file_from_string($record, $content);
236         } else {
237             // This was a combined pdf.
238             $file = $fs->create_file_from_pathname($record, $tmpfile);
239             @unlink($tmpfile);
240         }
242         return $file;
243     }
245     /**
246      * This function will generate and return a list of the page images from a pdf.
247      * @param int|\assign $assignment
248      * @param int $userid
249      * @param int $attemptnumber (-1 means latest attempt)
250      * @return array(stored_file)
251      */
252     public static function generate_page_images_for_attempt($assignment, $userid, $attemptnumber) {
253         global $CFG;
255         require_once($CFG->libdir . '/pdflib.php');
257         $assignment = self::get_assignment_from_param($assignment);
259         if (!$assignment->can_view_submission($userid)) {
260             \print_error('nopermission');
261         }
263         // Need to generate the page images - first get a combined pdf.
264         $file = self::get_combined_pdf_for_attempt($assignment, $userid, $attemptnumber);
265         if (!$file) {
266             throw \moodle_exception('Could not generate combined pdf.');
267         }
269         $tmpdir = \make_temp_directory('assignfeedback_editpdf/pageimages/' . self::hash($assignment, $userid, $attemptnumber));
270         $combined = $tmpdir . '/' . self::COMBINED_PDF_FILENAME;
271         $file->copy_content_to($combined); // Copy the file.
273         $pdf = new pdf();
275         $pdf->set_image_folder($tmpdir);
276         $pagecount = $pdf->set_pdf($combined);
278         $i = 0;
279         $images = array();
280         for ($i = 0; $i < $pagecount; $i++) {
281             $images[$i] = $pdf->get_image($i);
282         }
283         $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
285         $files = array();
286         $record = new \stdClass();
287         $record->contextid = $assignment->get_context()->id;
288         $record->component = 'assignfeedback_editpdf';
289         $record->filearea = self::PAGE_IMAGE_FILEAREA;
290         $record->itemid = $grade->id;
291         $record->filepath = '/';
292         $fs = \get_file_storage();
294         foreach ($images as $index => $image) {
295             $record->filename = basename($image);
296             $files[$index] = $fs->create_file_from_pathname($record, $tmpdir . '/' . $image);
297             @unlink($tmpdir . '/' . $image);
298         }
299         @unlink($combined);
300         @rmdir($tmpdir);
302         return $files;
303     }
305     /**
306      * This function returns a list of the page images from a pdf.
307      * @param int|\assign $assignment
308      * @param int $userid
309      * @param int $attemptnumber (-1 means latest attempt)
310      * @return array(stored_file)
311      */
312     public static function get_page_images_for_attempt($assignment, $userid, $attemptnumber) {
314         $assignment = self::get_assignment_from_param($assignment);
316         if (!$assignment->can_view_submission($userid)) {
317             \print_error('nopermission');
318         }
320         if ($assignment->get_instance()->teamsubmission) {
321             $submission = $assignment->get_group_submission($userid, 0, false);
322         } else {
323             $submission = $assignment->get_user_submission($userid, false);
324         }
325         $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
327         $contextid = $assignment->get_context()->id;
328         $component = 'assignfeedback_editpdf';
329         $filearea = self::PAGE_IMAGE_FILEAREA;
330         $itemid = $grade->id;
331         $filepath = '/';
333         $fs = \get_file_storage();
335         $files = $fs->get_directory_files($contextid, $component, $filearea, $itemid, $filepath);
337         if (!empty($files)) {
338             $first = reset($files);
339             if ($first->get_timemodified() < $submission->timemodified) {
341                 $fs->delete_area_files($contextid, $component, $filearea, $itemid);
342                 // Image files are stale - regenerate them.
343                 $files = array();
344             } else {
345                 return $files;
346             }
347         }
348         return self::generate_page_images_for_attempt($assignment, $userid, $attemptnumber);
349     }
351     /**
352      * This function returns sensible filename for a feedback file.
353      * @param int|\assign $assignment
354      * @param int $userid
355      * @param int $attemptnumber (-1 means latest attempt)
356      * @return string
357      */
358     protected static function get_downloadable_feedback_filename($assignment, $userid, $attemptnumber) {
359         global $DB;
361         $assignment = self::get_assignment_from_param($assignment);
363         $groupmode = groups_get_activity_groupmode($assignment->get_course_module());
364         $groupname = '';
365         if ($groupmode) {
366             $groupid = groups_get_activity_group($assignment->get_course_module(), true);
367             $groupname = groups_get_group_name($groupid).'-';
368         }
369         if ($groupname == '-') {
370             $groupname = '';
371         }
372         $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
373         $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
375         if ($assignment->is_blind_marking()) {
376             $prefix = $groupname . get_string('participant', 'assign');
377             $prefix = str_replace('_', ' ', $prefix);
378             $prefix = clean_filename($prefix . '_' . $assignment->get_uniqueid_for_user($userid) . '_');
379         } else {
380             $prefix = $groupname . fullname($user);
381             $prefix = str_replace('_', ' ', $prefix);
382             $prefix = clean_filename($prefix . '_' . $assignment->get_uniqueid_for_user($userid) . '_');
383         }
384         $prefix .= $grade->attemptnumber;
386         return $prefix . '.pdf';
387     }
389     /**
390      * This function takes the combined pdf and embeds all the comments and annotations.
391      * @param int|\assign $assignment
392      * @param int $userid
393      * @param int $attemptnumber (-1 means latest attempt)
394      * @return stored_file
395      */
396     public static function generate_feedback_document($assignment, $userid, $attemptnumber) {
398         $assignment = self::get_assignment_from_param($assignment);
400         if (!$assignment->can_view_submission($userid)) {
401             \print_error('nopermission');
402         }
403         if (!$assignment->can_grade()) {
404             \print_error('nopermission');
405         }
407         // Need to generate the page images - first get a combined pdf.
408         $file = self::get_combined_pdf_for_attempt($assignment, $userid, $attemptnumber);
409         if (!$file) {
410             throw \moodle_exception('Could not generate combined pdf.');
411         }
413         $tmpdir = \make_temp_directory('assignfeedback_editpdf/final/' . self::hash($assignment, $userid, $attemptnumber));
414         $combined = $tmpdir . '/' . self::COMBINED_PDF_FILENAME;
415         $file->copy_content_to($combined); // Copy the file.
417         $pdf = new pdf();
419         $fs = \get_file_storage();
420         $stamptmpdir = \make_temp_directory('assignfeedback_editpdf/stamps/' . self::hash($assignment, $userid, $attemptnumber));
421         $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
422         // Copy any new stamps to this instance.
423         if ($files = $fs->get_area_files($assignment->get_context()->id,
424                                          'assignfeedback_editpdf',
425                                          'stamps',
426                                          $grade->id,
427                                          "filename",
428                                          false)) {
429             foreach ($files as $file) {
430                 $filename = $stamptmpdir . '/' . $file->get_filename();
431                 $file->copy_content_to($filename); // Copy the file.
432             }
433         }
435         $pagecount = $pdf->set_pdf($combined);
436         $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
437         page_editor::release_drafts($grade->id);
439         for ($i = 0; $i < $pagecount; $i++) {
440             $pdf->copy_page();
441             $comments = page_editor::get_comments($grade->id, $i, false);
442             $annotations = page_editor::get_annotations($grade->id, $i, false);
444             foreach ($comments as $comment) {
445                 $pdf->add_comment($comment->rawtext,
446                                   $comment->x,
447                                   $comment->y,
448                                   $comment->width,
449                                   $comment->colour);
450             }
452             foreach ($annotations as $annotation) {
453                 $pdf->add_annotation($annotation->x,
454                                      $annotation->y,
455                                      $annotation->endx,
456                                      $annotation->endy,
457                                      $annotation->colour,
458                                      $annotation->type,
459                                      $annotation->path,
460                                      $stamptmpdir);
461             }
462         }
464         fulldelete($stamptmpdir);
466         $filename = self::get_downloadable_feedback_filename($assignment, $userid, $attemptnumber);
467         $filename = clean_param($filename, PARAM_FILE);
469         $generatedpdf = $tmpdir . '/' . $filename;
470         $pdf->save_pdf($generatedpdf);
473         $record = new \stdClass();
475         $record->contextid = $assignment->get_context()->id;
476         $record->component = 'assignfeedback_editpdf';
477         $record->filearea = self::FINAL_PDF_FILEAREA;
478         $record->itemid = $grade->id;
479         $record->filepath = '/';
480         $record->filename = $filename;
483         // Only keep one current version of the generated pdf.
484         $fs->delete_area_files($record->contextid, $record->component, $record->filearea, $record->itemid);
486         $file = $fs->create_file_from_pathname($record, $generatedpdf);
488         // Cleanup.
489         @unlink($generatedpdf);
490         @unlink($combined);
491         @rmdir($tmpdir);
493         return $file;
494     }
496     /**
497      * This function returns the generated pdf (if it exists).
498      * @param int|\assign $assignment
499      * @param int $userid
500      * @param int $attemptnumber (-1 means latest attempt)
501      * @return stored_file
502      */
503     public static function get_feedback_document($assignment, $userid, $attemptnumber) {
505         $assignment = self::get_assignment_from_param($assignment);
507         if (!$assignment->can_view_submission($userid)) {
508             \print_error('nopermission');
509         }
511         $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
513         $contextid = $assignment->get_context()->id;
514         $component = 'assignfeedback_editpdf';
515         $filearea = self::FINAL_PDF_FILEAREA;
516         $itemid = $grade->id;
517         $filepath = '/';
519         $fs = \get_file_storage();
520         $files = $fs->get_area_files($contextid,
521                                      $component,
522                                      $filearea,
523                                      $itemid,
524                                      "itemid, filepath, filename",
525                                      false);
526         if ($files) {
527             return reset($files);
528         }
529         return false;
530     }
532     /**
533      * This function deletes the generated pdf for a student.
534      * @param int|\assign $assignment
535      * @param int $userid
536      * @param int $attemptnumber (-1 means latest attempt)
537      * @return bool
538      */
539     public static function delete_feedback_document($assignment, $userid, $attemptnumber) {
541         $assignment = self::get_assignment_from_param($assignment);
543         if (!$assignment->can_view_submission($userid)) {
544             \print_error('nopermission');
545         }
546         if (!$assignment->can_grade()) {
547             \print_error('nopermission');
548         }
550         $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
552         $contextid = $assignment->get_context()->id;
553         $component = 'assignfeedback_editpdf';
554         $filearea = self::FINAL_PDF_FILEAREA;
555         $itemid = $grade->id;
557         $fs = \get_file_storage();
558         return $fs->delete_area_files($contextid, $component, $filearea, $itemid);
559     }