MDL-25619 portfolio: exported HTML output can not be cleaned
[moodle.git] / mod / glossary / locallib.php
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
18 /**
19  * Library of functions and constants for module glossary
20  * outside of what is required for the core moodle api
21  *
22  * @package   mod-glossary
23  * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
24  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25  */
27 require_once($CFG->libdir . '/portfolio/caller.php');
29 /**
30  * class to handle exporting an entire glossary database
31  */
32 class glossary_full_portfolio_caller extends portfolio_module_caller_base {
34     private $glossary;
35     private $exportdata;
36     private $keyedfiles = array(); // keyed on entry
38     /**
39      * return array of expected call back arguments
40      * and whether they are required or not
41      *
42      * @return array
43      */
44     public static function expected_callbackargs() {
45         return array(
46             'id' => true,
47         );
48     }
50     /**
51      * load up all data required for this export.
52      *
53      * @return void
54      */
55     public function load_data() {
56         global $DB;
57         if (!$this->cm = get_coursemodule_from_id('glossary', $this->id)) {
58             throw new portfolio_caller_exception('invalidid', 'glossary');
59         }
60         if (!$this->glossary = $DB->get_record('glossary', array('id' => $this->cm->instance))) {
61             throw new portfolio_caller_exception('invalidid', 'glossary');
62         }
63         $entries = $DB->get_records('glossary_entries', array('glossaryid' => $this->glossary->id));
64         list($where, $params) = $DB->get_in_or_equal(array_keys($entries));
66         $aliases = $DB->get_records_select('glossary_alias', 'entryid ' . $where, $params);
67         $categoryentries = $DB->get_records_sql('SELECT ec.entryid, c.name FROM {glossary_entries_categories} ec
68             JOIN {glossary_categories} c
69             ON c.id = ec.categoryid
70             WHERE ec.entryid ' . $where, $params);
72         $this->exportdata = array('entries' => $entries, 'aliases' => $aliases, 'categoryentries' => $categoryentries);
73         $fs = get_file_storage();
74         $context = get_context_instance(CONTEXT_MODULE, $this->cm->id);
75         $this->multifiles = array();
76         foreach (array_keys($entries) as $entry) {
77             $this->keyedfiles[$entry] = array_merge(
78                 $fs->get_area_files($context->id, 'mod_glossary', 'attachment', $entry, "timemodified", false),
79                 $fs->get_area_files($context->id, 'mod_glossary', 'entry', $entry, "timemodified", false)
80             );
81             $this->multifiles = array_merge($this->multifiles, $this->keyedfiles[$entry]);
82         }
83     }
85     /**
86      * how long might we expect this export to take
87      *
88      * @return constant one of PORTFOLIO_TIME_XX
89      */
90     public function expected_time() {
91         $filetime = portfolio_expected_time_file($this->multifiles);
92         $dbtime   = portfolio_expected_time_db(count($this->exportdata['entries']));
93         return ($filetime > $dbtime) ? $filetime : $dbtime;
94     }
96     /**
97      * return the sha1 of this content
98      *
99      * @return string
100      */
101     public function get_sha1() {
102         $file = '';
103         if ($this->multifiles) {
104             $file = $this->get_sha1_file();
105         }
106         return sha1(serialize($this->exportdata) . $file);
107     }
109     /**
110      * prepare the package ready to be passed off to the portfolio plugin
111      *
112      * @return void
113      */
114     public function prepare_package() {
115         $entries = $this->exportdata['entries'];
116         $aliases = array();
117         $categories = array();
118         if (is_array($this->exportdata['aliases'])) {
119             foreach ($this->exportdata['aliases'] as $alias) {
120                 if (!array_key_exists($alias->entryid, $aliases)) {
121                     $aliases[$alias->entryid] = array();
122                 }
123                 $aliases[$alias->entryid][] = $alias->alias;
124             }
125         }
126         if (is_array($this->exportdata['categoryentries'])) {
127             foreach ($this->exportdata['categoryentries'] as $cat) {
128                 if (!array_key_exists($cat->entryid, $categories)) {
129                     $categories[$cat->entryid] = array();
130                 }
131                 $categories[$cat->entryid][] = $cat->name;
132             }
133         }
134         if ($this->get('exporter')->get('formatclass') == PORTFOLIO_FORMAT_SPREADSHEET) {
135             $csv = glossary_generate_export_csv($entries, $aliases, $categories);
136             $this->exporter->write_new_file($csv, clean_filename($this->cm->name) . '.csv', false);
137             return;
138         } else if ($this->get('exporter')->get('formatclass') == PORTFOLIO_FORMAT_LEAP2A) {
139             $ids = array(); // keep track of these to make into a selection later
140             global $USER, $DB;
141             $writer = $this->get('exporter')->get('format')->leap2a_writer($USER);
142             $format = $this->exporter->get('format');
143             $filename = $this->get('exporter')->get('format')->manifest_name();
144             foreach ($entries as $e) {
145                 $content = glossary_entry_portfolio_caller::entry_content(
146                     $this->course,
147                     $this->cm,
148                     $this->glossary,
149                     $e,
150                     (array_key_exists($e->id, $aliases) ? $aliases[$e->id] : array()),
151                     $format
152                 );
153                 $entry = new portfolio_format_leap2a_entry('glossaryentry' . $e->id, $e->concept, 'entry', $content);
154                 $entry->author    = $DB->get_record('user', array('id' => $e->userid), 'id,firstname,lastname,email');
155                 $entry->published = $e->timecreated;
156                 $entry->updated   = $e->timemodified;
157                 if (!empty($this->keyedfiles[$e->id])) {
158                     $writer->link_files($entry, $this->keyedfiles[$e->id], 'glossaryentry' . $e->id . 'file');
159                     foreach ($this->keyedfiles[$e->id] as $file) {
160                         $this->exporter->copy_existing_file($file);
161                     }
162                 }
163                 if (!empty($categories[$e->id])) {
164                     foreach ($categories[$e->id] as $cat) {
165                         // this essentially treats them as plain tags
166                         // leap has the idea of category schemes
167                         // but I think this is overkill here
168                         $entry->add_category($cat);
169                     }
170                 }
171                 $writer->add_entry($entry);
172                 $ids[] = $entry->id;
173             }
174             $selection = new portfolio_format_leap2a_entry('wholeglossary' . $this->glossary->id, get_string('modulename', 'glossary'), 'selection');
175             $writer->add_entry($selection);
176             $writer->make_selection($selection, $ids, 'Grouping');
177             $content = $writer->to_xml();
178         }
179         $this->exporter->write_new_file($content, $filename, true);
180     }
182     /**
183      * make sure that the current user is allowed to do this
184      *
185      * @return boolean
186      */
187     public function check_permissions() {
188         return has_capability('mod/glossary:export', get_context_instance(CONTEXT_MODULE, $this->cm->id));
189     }
191     /**
192      * return a nice name to be displayed about this export location
193      *
194      * @return string
195      */
196     public static function display_name() {
197         return get_string('modulename', 'glossary');
198     }
200     /**
201      * what formats this function *generally* supports
202      *
203      * @return array
204      */
205     public static function base_supported_formats() {
206         return array(PORTFOLIO_FORMAT_SPREADSHEET, PORTFOLIO_FORMAT_LEAP2A);
207     }
210 /**
211  * class to export a single glossary entry
212  *
213  * @package   mod-glossary
214  * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
215  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
216  */
217 class glossary_entry_portfolio_caller extends portfolio_module_caller_base {
219     private $glossary;
220     private $entry;
221     protected $entryid;
222     /*
223      * @return array
224      */
225     public static function expected_callbackargs() {
226         return array(
227             'entryid' => true,
228             'id'      => true,
229         );
230     }
232     /**
233      * load up all data required for this export.
234      *
235      * @return void
236      */
237     public function load_data() {
238         global $DB;
239         if (!$this->cm = get_coursemodule_from_id('glossary', $this->id)) {
240             throw new portfolio_caller_exception('invalidid', 'glossary');
241         }
242         if (!$this->glossary = $DB->get_record('glossary', array('id' => $this->cm->instance))) {
243             throw new portfolio_caller_exception('invalidid', 'glossary');
244         }
245         if ($this->entryid) {
246             if (!$this->entry = $DB->get_record('glossary_entries', array('id' => $this->entryid))) {
247                 throw new portfolio_caller_exception('noentry', 'glossary');
248             }
249             // in case we don't have USER this will make the entry be printed
250             $this->entry->approved = true;
251         }
252         $this->categories = $DB->get_records_sql('SELECT ec.entryid, c.name FROM {glossary_entries_categories} ec
253             JOIN {glossary_categories} c
254             ON c.id = ec.categoryid
255             WHERE ec.entryid = ?', array($this->entryid));
256         $context = get_context_instance(CONTEXT_MODULE, $this->cm->id);
257         if ($this->entry->sourceglossaryid == $this->cm->instance) {
258             if ($maincm = get_coursemodule_from_instance('glossary', $this->entry->glossaryid)) {
259                 $context = get_context_instance(CONTEXT_MODULE, $maincm->id);
260             }
261         }
262         $this->aliases = $DB->get_record('glossary_alias', array('entryid'=>$this->entryid));
263         $fs = get_file_storage();
264         $this->multifiles = array_merge(
265             $fs->get_area_files($context->id, 'mod_glossary', 'attachment', $this->entry->id, "timemodified", false),
266             $fs->get_area_files($context->id, 'mod_glossary', 'entry', $this->entry->id, "timemodified", false)
267         );
268     }
270     /**
271      * how long might we expect this export to take
272      *
273      * @return constant one of PORTFOLIO_TIME_XX
274      */
275     public function expected_time() {
276         return PORTFOLIO_TIME_LOW;
277     }
279     /**
280      * make sure that the current user is allowed to do this
281      *
282      * @return boolean
283      */
284     public function check_permissions() {
285         $context = get_context_instance(CONTEXT_MODULE, $this->cm->id);
286         return has_capability('mod/glossary:exportentry', $context)
287             || ($this->entry->userid == $this->user->id && has_capability('mod/glossary:exportownentry', $context));
288     }
290     /**
291      * return a nice name to be displayed about this export location
292      *
293      * @return string
294      */
295     public static function display_name() {
296         return get_string('modulename', 'glossary');
297     }
299     /**
300      * prepare the package ready to be passed off to the portfolio plugin
301      *
302      * @return void
303      */
304     public function prepare_package() {
305         global $DB;
306         $format = $this->exporter->get('format');
307         $content = self::entry_content($this->course, $this->cm, $this->glossary, $this->entry, $this->aliases, $format);
308         $filename = clean_filename($this->entry->concept) . '.html';
309         if ($this->exporter->get('formatclass') == PORTFOLIO_FORMAT_LEAP2A) {
310             global $USER;
311             $writer = $this->get('exporter')->get('format')->leap2a_writer($USER);
312             $filename = $this->get('exporter')->get('format')->manifest_name();
313             $entry = new portfolio_format_leap2a_entry('glossaryentry' . $this->entry->id, $this->entry->concept, 'entry', $content);
314             $entry->author = $DB->get_record('user', array('id' => $this->entry->userid), 'id,firstname,lastname,email');
315             $entry->published = $this->entry->timecreated;
316             $entry->updated = $this->entry->timemodified;
317             if ($this->multifiles) {
318                 $writer->link_files($entry, $this->multifiles, 'glossaryentry' . $this->entry->id . 'file');
319             }
320             if ($this->categories) {
321                 foreach ($this->categories as $cat) {
322                     // this essentially treats them as plain tags
323                     // leap has the idea of category schemes
324                     // but I think this is overkill here
325                     $entry->add_category($cat->name);
326                 }
327             }
328             $writer->add_entry($entry);
329             $content = $writer->to_xml();
330         }
331         if ($this->multifiles) {
332             foreach ($this->multifiles as $file) {
333                 $this->exporter->copy_existing_file($file);
334             }
335         }
336         return $this->exporter->write_new_file($content, $filename, !empty($this->multifiles));
337     }
339     /**
340      * return the sha1 of this content
341      *
342      * @return string
343      */
344     public function get_sha1() {
345         if ($this->multifiles) {
346             return sha1(serialize($this->entry) . $this->get_sha1_file());
347         }
348         return sha1(serialize($this->entry));
349     }
351     /**
352      * what formats this function *generally* supports
353      *
354      * @return array
355      */
356     public static function base_supported_formats() {
357         return array(PORTFOLIO_FORMAT_RICHHTML, PORTFOLIO_FORMAT_PLAINHTML, PORTFOLIO_FORMAT_LEAP2A);
358     }
360     /**
361      * helper function to get the html content of an entry
362      * for both this class and the full glossary exporter
363      * this is a very simplified version of the dictionary format output,
364      * but with its 500 levels of indirection removed
365      * and file rewriting handled by the portfolio export format.
366      *
367      * @param stdclass $course
368      * @param stdclass $cm
369      * @param stdclass $glossary
370      * @param stdclass $entry
371      *
372      * @return string
373      */
374     public static function entry_content($course, $cm, $glossary, $entry, $aliases, $format) {
375         global $OUTPUT, $DB;
376         $entry = clone $entry;
377         $context = get_context_instance(CONTEXT_MODULE, $cm->id);
378         $options = portfolio_format_text_options();
379         $options->trusted = $entry->definitiontrust;
380         $options->context = $context;
382         $output = '<table class="glossarypost dictionary" cellspacing="0">' . "\n";
383         $output .= '<tr valign="top">' . "\n";
384         $output .= '<td class="entry">' . "\n";
386         $output .= '<div class="concept">';
387         $output .= format_text($OUTPUT->heading($entry->concept, 3), FORMAT_MOODLE, $options);
388         $output .= '</div> ' . "\n";
390         $entry->definition = format_text($entry->definition, $entry->definitionformat, $options);
391         $output .= portfolio_rewrite_pluginfile_urls($entry->definition, $context->id, 'mod_glossary', 'entry', $entry->id, $format);
393         if (isset($entry->footer)) {
394             $output .= $entry->footer;
395         }
397         $output .= '</td></tr>' . "\n";
399         if (!empty($aliases)) {
400             $aliases = explode(',', $aliases->alias);
401             $output .= '<tr valign="top"><td class="entrylowersection">';
402             $key = (count($aliases) == 1) ? 'alias' : 'aliases';
403             $output .= get_string($key, 'glossary') . ': ';
404             foreach ($aliases as $alias) {
405                 $output .= s($alias) . ',';
406             }
407             $output = substr($output, 0, -1);
408             $output .= '</td></tr>' . "\n";
409         }
411         if ($entry->sourceglossaryid == $cm->instance) {
412             if (!$maincm = get_coursemodule_from_instance('glossary', $entry->glossaryid)) {
413                 return '';
414             }
415             $filecontext = get_context_instance(CONTEXT_MODULE, $maincm->id);
417         } else {
418             $filecontext = $context;
419         }
420         $fs = get_file_storage();
421         if ($files = $fs->get_area_files($filecontext->id, 'mod_glossary', 'attachment', $entry->id, "timemodified", false)) {
422             $output .= '<table border="0" width="100%"><tr><td>' . "\n";
424             foreach ($files as $file) {
425                 $output .= $format->file_output($file);
426             }
427             $output .= '</td></tr></table>' . "\n";
428         }
430         $output .= '</table>' . "\n";
432         return $output;
433     }