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 definition for the library class for PDF feedback plugin
19 *
20 *
21 * @package   assignfeedback_editpdf
22 * @copyright 2012 Davo Smith
23 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24 */
25
26defined('MOODLE_INTERNAL') || die();
27
28use \assignfeedback_editpdf\document_services;
29use \assignfeedback_editpdf\page_editor;
30
31/**
32 * library class for editpdf feedback plugin extending feedback plugin base class
33 *
34 * @package   assignfeedback_editpdf
35 * @copyright 2012 Davo Smith
36 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
37 */
38class assign_feedback_editpdf extends assign_feedback_plugin {
39
40    /** @var boolean|null $enabledcache Cached lookup of the is_enabled function */
41    private $enabledcache = null;
42
43    /**
44     * Get the name of the file feedback plugin
45     * @return string
46     */
47    public function get_name() {
48        return get_string('pluginname', 'assignfeedback_editpdf');
49    }
50
51    /**
52     * Create a widget for rendering the editor.
53     *
54     * @param int $userid
55     * @param stdClass $grade
56     * @param bool $readonly
57     * @return assignfeedback_editpdf_widget
58     */
59    public function get_widget($userid, $grade, $readonly) {
60        $attempt = -1;
61        if ($grade && isset($grade->attemptnumber)) {
62            $attempt = $grade->attemptnumber;
63        } else {
64            $grade = $this->assignment->get_user_grade($userid, true);
65        }
66
67        $feedbackfile = document_services::get_feedback_document(
68            $this->assignment->get_instance()->id,
69            $userid,
70            $attempt
71        );
72
73        $stampfiles = array();
74        $fs = get_file_storage();
75        $syscontext = context_system::instance();
76        $asscontext = $this->assignment->get_context();
77
78        // Three file areas are used for stamps.
79        // Current stamps are those configured as a site administration setting to be available for new uses.
80        // When a stamp is removed from this filearea it is no longer available for new grade items.
81        $currentstamps = $fs->get_area_files($syscontext->id, 'assignfeedback_editpdf', 'stamps', 0, 'filename', false);
82
83        // Grade stamps are those which have been assigned for a specific grade item.
84        // The stamps associated with a grade item are always used for that grade item, even if the stamp is removed
85        // from the list of current stamps.
86        $gradestamps = $fs->get_area_files($asscontext->id, 'assignfeedback_editpdf', 'stamps', $grade->id, 'filename', false);
87
88        // The system stamps are perpetual and always exist.
89        // They allow Moodle to serve a common URL for all users for any possible combination of stamps.
90        // Files in the perpetual stamp filearea are within the system context, in itemid 0, and use the original stamps
91        // contenthash as a folder name. This ensures that the combination of stamp filename, and stamp file content is
92        // unique.
93        $systemstamps = $fs->get_area_files($syscontext->id, 'assignfeedback_editpdf', 'systemstamps', 0, 'filename', false);
94
95        // First check that all current stamps are listed in the grade stamps.
96        foreach ($currentstamps as $stamp) {
97            // Ensure that the current stamp is in the list of perpetual stamps.
98            $systempathnamehash = $this->get_system_stamp_path($stamp);
99            if (!array_key_exists($systempathnamehash, $systemstamps)) {
100                $filerecord = (object) [
101                    'filearea' => 'systemstamps',
102                    'filepath' => '/' . $stamp->get_contenthash() . '/',
103                ];
104                $newstamp = $fs->create_file_from_storedfile($filerecord, $stamp);
105                $systemstamps[$newstamp->get_pathnamehash()] = $newstamp;
106            }
107
108            // Ensure that the current stamp is in the list of stamps for the current grade item.
109            $gradeitempathhash = $this->get_assignment_stamp_path($stamp, $grade->id);
110            if (!array_key_exists($gradeitempathhash, $gradestamps)) {
111                $filerecord = (object) [
112                    'contextid' => $asscontext->id,
113                    'filearea' => 'stamps',
114                    'itemid' => $grade->id,
115                ];
116                $newstamp = $fs->create_file_from_storedfile($filerecord, $stamp);
117                $gradestamps[$newstamp->get_pathnamehash()] = $newstamp;
118            }
119        }
120
121        foreach ($gradestamps as $stamp) {
122            // All gradestamps should be available in the systemstamps filearea, but some legacy stamps may not be.
123            // These need to be copied over.
124            // Note: This should ideally be performed as an upgrade step, but there may be other cases that these do not
125            // exist, for example restored backups.
126            // In any case this is a cheap operation as it is solely performing an array lookup.
127            $systempathnamehash = $this->get_system_stamp_path($stamp);
128            if (!array_key_exists($systempathnamehash, $systemstamps)) {
129                $filerecord = (object) [
130                    'contextid' => $syscontext->id,
131                    'itemid' => 0,
132                    'filearea' => 'systemstamps',
133                    'filepath' => '/' . $stamp->get_contenthash() . '/',
134                ];
135                $systemstamp = $fs->create_file_from_storedfile($filerecord, $stamp);
136                $systemstamps[$systemstamp->get_pathnamehash()] = $systemstamp;
137            }
138
139            // Always serve the perpetual system stamp.
140            // This ensures that the stamp is highly cached and reduces the hit on the application server.
141            $gradestamp = $systemstamps[$systempathnamehash];
142            $url = moodle_url::make_pluginfile_url(
143                $gradestamp->get_contextid(),
144                $gradestamp->get_component(),
145                $gradestamp->get_filearea(),
146                null,
147                $gradestamp->get_filepath(),
148                $gradestamp->get_filename(),
149                false
150            );
151            array_push($stampfiles, $url->out());
152        }
153
154        $url = false;
155        $filename = '';
156        if ($feedbackfile) {
157            $url = moodle_url::make_pluginfile_url(
158                $this->assignment->get_context()->id,
159                'assignfeedback_editpdf',
160                document_services::FINAL_PDF_FILEAREA,
161                $grade->id,
162                '/',
163                $feedbackfile->get_filename(),
164                false
165            );
166           $filename = $feedbackfile->get_filename();
167        }
168
169        $widget = new assignfeedback_editpdf_widget(
170            $this->assignment->get_instance()->id,
171            $userid,
172            $attempt,
173            $url,
174            $filename,
175            $stampfiles,
176            $readonly
177        );
178        return $widget;
179    }
180
181    /**
182     * Get the pathnamehash for the specified stamp if in the system stamps.
183     *
184     * @param   stored_file $file
185     * @return  string
186     */
187    protected function get_system_stamp_path(stored_file $stamp): string {
188        $systemcontext = context_system::instance();
189
190        return file_storage::get_pathname_hash(
191            $systemcontext->id,
192            'assignfeedback_editpdf',
193            'systemstamps',
194            0,
195            '/' . $stamp->get_contenthash() . '/',
196            $stamp->get_filename()
197        );
198    }
199
200    /**
201     * Get the pathnamehash for the specified stamp if in the current assignment stamps.
202     *
203     * @param   stored_file $file
204     * @param   int $gradeid
205     * @return  string
206     */
207    protected function get_assignment_stamp_path(stored_file $stamp, int $gradeid): string {
208        return file_storage::get_pathname_hash(
209            $this->assignment->get_context()->id,
210            'assignfeedback_editpdf',
211            'stamps',
212            $gradeid,
213            $stamp->get_filepath(),
214            $stamp->get_filename()
215        );
216    }
217
218    /**
219     * Get form elements for grading form
220     *
221     * @param stdClass $grade
222     * @param MoodleQuickForm $mform
223     * @param stdClass $data
224     * @param int $userid
225     * @return bool true if elements were added to the form
226     */
227    public function get_form_elements_for_user($grade, MoodleQuickForm $mform, stdClass $data, $userid) {
228        global $PAGE;
229
230        $attempt = -1;
231        if ($grade) {
232            $attempt = $grade->attemptnumber;
233        }
234
235        $renderer = $PAGE->get_renderer('assignfeedback_editpdf');
236
237        // Links to download the generated pdf...
238        if ($attempt > -1 && page_editor::has_annotations_or_comments($grade->id, false)) {
239            $html = $this->assignment->render_area_files('assignfeedback_editpdf',
240                                                         document_services::FINAL_PDF_FILEAREA,
241                                                         $grade->id);
242            $mform->addElement('static', 'editpdf_files', get_string('downloadfeedback', 'assignfeedback_editpdf'), $html);
243        }
244
245        $widget = $this->get_widget($userid, $grade, false);
246
247        $html = $renderer->render($widget);
248        $mform->addElement('static', 'editpdf', get_string('editpdf', 'assignfeedback_editpdf'), $html);
249        $mform->addHelpButton('editpdf', 'editpdf', 'assignfeedback_editpdf');
250        $mform->addElement('hidden', 'editpdf_source_userid', $userid);
251        $mform->setType('editpdf_source_userid', PARAM_INT);
252        $mform->setConstant('editpdf_source_userid', $userid);
253    }
254
255    /**
256     * Check to see if the grade feedback for the pdf has been modified.
257     *
258     * @param stdClass $grade Grade object.
259     * @param stdClass $data Data from the form submission (not used).
260     * @return boolean True if the pdf has been modified, else false.
261     */
262    public function is_feedback_modified(stdClass $grade, stdClass $data) {
263        // We only need to know if the source user's PDF has changed. If so then all
264        // following users will have the same status. If it's only an individual annotation
265        // then only one user will come through this method.
266        // Source user id is only added to the form if there was a pdf.
267        if (!empty($data->editpdf_source_userid)) {
268            $sourceuserid = $data->editpdf_source_userid;
269            // Retrieve the grade information for the source user.
270            $sourcegrade = $this->assignment->get_user_grade($sourceuserid, true, $grade->attemptnumber);
271            $pagenumbercount = document_services::page_number_for_attempt($this->assignment, $sourceuserid, $sourcegrade->attemptnumber);
272            for ($i = 0; $i < $pagenumbercount; $i++) {
273                // Select all annotations.
274                $draftannotations = page_editor::get_annotations($sourcegrade->id, $i, true);
275                $nondraftannotations = page_editor::get_annotations($grade->id, $i, false);
276                // Check to see if the count is the same.
277                if (count($draftannotations) != count($nondraftannotations)) {
278                    // The count is different so we have a modification.
279                    return true;
280                } else {
281                    $matches = 0;
282                    // Have a closer look and see if the draft files match all the non draft files.
283                    foreach ($nondraftannotations as $ndannotation) {
284                        foreach ($draftannotations as $dannotation) {
285                            foreach ($ndannotation as $key => $value) {
286                                if ($key != 'id' && $value != $dannotation->{$key}) {
287                                    continue 2;
288                                }
289                            }
290                            $matches++;
291                        }
292                    }
293                    if ($matches !== count($nondraftannotations)) {
294                        return true;
295                    }
296                }
297                // Select all comments.
298                $draftcomments = page_editor::get_comments($sourcegrade->id, $i, true);
299                $nondraftcomments = page_editor::get_comments($grade->id, $i, false);
300                if (count($draftcomments) != count($nondraftcomments)) {
301                    return true;
302                } else {
303                    // Go for a closer inspection.
304                    $matches = 0;
305                    foreach ($nondraftcomments as $ndcomment) {
306                        foreach ($draftcomments as $dcomment) {
307                            foreach ($ndcomment as $key => $value) {
308                                if ($key != 'id' && $value != $dcomment->{$key}) {
309                                    continue 2;
310                                }
311                            }
312                            $matches++;
313                        }
314                    }
315                    if ($matches !== count($nondraftcomments)) {
316                        return true;
317                    }
318                }
319            }
320        }
321        return false;
322    }
323
324    /**
325     * Generate the pdf.
326     *
327     * @param stdClass $grade
328     * @param stdClass $data
329     * @return bool
330     */
331    public function save(stdClass $grade, stdClass $data) {
332        // Source user id is only added to the form if there was a pdf.
333        if (!empty($data->editpdf_source_userid)) {
334            $sourceuserid = $data->editpdf_source_userid;
335            // Copy drafts annotations and comments if current user is different to sourceuserid.
336            if ($sourceuserid != $grade->userid) {
337                page_editor::copy_drafts_from_to($this->assignment, $grade, $sourceuserid);
338            }
339        }
340        if (page_editor::has_annotations_or_comments($grade->id, true)) {
341            document_services::generate_feedback_document($this->assignment, $grade->userid, $grade->attemptnumber);
342        }
343
344        return true;
345    }
346
347    /**
348     * Display the list of files in the feedback status table.
349     *
350     * @param stdClass $grade
351     * @param bool $showviewlink (Always set to false).
352     * @return string
353     */
354    public function view_summary(stdClass $grade, & $showviewlink) {
355        $showviewlink = false;
356        return $this->view($grade);
357    }
358
359    /**
360     * Display the list of files in the feedback status table.
361     *
362     * @param stdClass $grade
363     * @return string
364     */
365    public function view(stdClass $grade) {
366        global $PAGE;
367        $html = '';
368        // Show a link to download the pdf.
369        if (page_editor::has_annotations_or_comments($grade->id, false)) {
370            $html = $this->assignment->render_area_files('assignfeedback_editpdf',
371                                                         document_services::FINAL_PDF_FILEAREA,
372                                                         $grade->id);
373
374            // Also show the link to the read-only interface.
375            $renderer = $PAGE->get_renderer('assignfeedback_editpdf');
376            $widget = $this->get_widget($grade->userid, $grade, true);
377
378            $html .= $renderer->render($widget);
379        }
380        return $html;
381    }
382
383    /**
384     * Return true if there are no released comments/annotations.
385     *
386     * @param stdClass $grade
387     */
388    public function is_empty(stdClass $grade) {
389        global $DB;
390
391        $comments = $DB->count_records('assignfeedback_editpdf_cmnt', array('gradeid'=>$grade->id, 'draft'=>0));
392        $annotations = $DB->count_records('assignfeedback_editpdf_annot', array('gradeid'=>$grade->id, 'draft'=>0));
393        return $comments == 0 && $annotations == 0;
394    }
395
396    /**
397     * The assignment has been deleted - remove the plugin specific data
398     *
399     * @return bool
400     */
401    public function delete_instance() {
402        global $DB;
403        $grades = $DB->get_records('assign_grades', array('assignment'=>$this->assignment->get_instance()->id), '', 'id');
404        if ($grades) {
405            list($gradeids, $params) = $DB->get_in_or_equal(array_keys($grades), SQL_PARAMS_NAMED);
406            $DB->delete_records_select('assignfeedback_editpdf_annot', 'gradeid ' . $gradeids, $params);
407            $DB->delete_records_select('assignfeedback_editpdf_cmnt', 'gradeid ' . $gradeids, $params);
408        }
409        return true;
410    }
411
412    /**
413     * Determine if ghostscript is available and working.
414     *
415     * @return bool
416     */
417    public function is_available() {
418        if ($this->enabledcache === null) {
419            $testpath = assignfeedback_editpdf\pdf::test_gs_path(false);
420            $this->enabledcache = ($testpath->status == assignfeedback_editpdf\pdf::GSPATH_OK);
421        }
422        return $this->enabledcache;
423    }
424    /**
425     * Prevent enabling this plugin if ghostscript is not available.
426     *
427     * @return bool false
428     */
429    public function is_configurable() {
430        return $this->is_available();
431    }
432
433    /**
434     * Get file areas returns a list of areas this plugin stores files.
435     *
436     * @return array - An array of fileareas (keys) and descriptions (values)
437     */
438    public function get_file_areas() {
439        return array(document_services::FINAL_PDF_FILEAREA => $this->get_name());
440    }
441
442    /**
443     * This plugin will inject content into the review panel with javascript.
444     * @return bool true
445     */
446    public function supports_review_panel() {
447        return true;
448    }
449
450    /**
451     * Return the plugin configs for external functions.
452     *
453     * @return array the list of settings
454     * @since Moodle 3.2
455     */
456    public function get_config_for_external() {
457        return (array) $this->get_config();
458    }
459}
460