Commit | Line | Data |
---|---|---|
59dd457e PL |
1 | <?php |
2 | /** | |
3 | * Moodle - Modular Object-Oriented Dynamic Learning Environment | |
4 | * http://moodle.org | |
5 | * Copyright (C) 1999 onwards Martin Dougiamas http://dougiamas.com | |
6 | * | |
7 | * This program is free software: you can redistribute it and/or modify | |
8 | * it under the terms of the GNU General Public License as published by | |
9 | * the Free Software Foundation, either version 2 of the License, or | |
10 | * (at your option) any later version. | |
11 | * | |
12 | * This program is distributed in the hope that it will be useful, | |
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
15 | * GNU General Public License for more details. | |
16 | * | |
17 | * You should have received a copy of the GNU General Public License | |
18 | * along with this program. If not, see <http://www.gnu.org/licenses/>. | |
19 | * | |
0972665f | 20 | * @package core |
59dd457e PL |
21 | * @subpackage portfolio |
22 | * @author Penny Leach <penny@liip.ch> | |
23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL | |
24 | * @copyright (C) 1999 onwards Martin Dougiamas http://dougiamas.com | |
25 | * | |
26 | * This file contains the LEAP2a writer used by portfolio_format_leap2a | |
27 | */ | |
28 | ||
0972665f PS |
29 | defined('MOODLE_INTERNAL') || die(); |
30 | ||
59dd457e PL |
31 | /** |
32 | * object to encapsulate the writing of leap2a. | |
33 | * should be used like: | |
34 | * | |
35 | * $writer = portfolio_format_leap2a::leap2a_writer($USER); | |
36 | * $entry = new portfolio_format_leap2a_entry('forumpost6', $title, 'leaptype', 'somecontent') | |
37 | * $entry->add_link('something', 'has_part')->add_link('somethingelse', 'has_part'); | |
38 | * .. etc | |
39 | * $writer->add_entry($entry); | |
40 | * $xmlstr = $writer->to_xml(); | |
41 | * | |
42 | * @TODO find a way to ensure that all referenced files are included | |
43 | */ | |
44 | class portfolio_format_leap2a_writer { | |
45 | ||
46 | /** the domdocument object used to create elements */ | |
47 | private $dom; | |
48 | /** the top level feed element */ | |
49 | private $feed; | |
50 | /** the user exporting data */ | |
51 | private $user; | |
52 | /** the id of the feed - this is unique to the user and date and used for portfolio ns as well as feed id */ | |
53 | private $id; | |
54 | /** the entries for the feed - keyed on id */ | |
55 | private $entries = array(); | |
56 | ||
57 | /** | |
58 | * constructor - usually generated from portfolio_format_leap2a::leap2a_writer($USER); | |
59 | * | |
60 | * @param stdclass $user the user exporting (almost always $USER) | |
61 | * | |
62 | */ | |
63 | public function __construct(stdclass $user) { // todo something else - exporter, format, etc | |
64 | global $CFG; | |
65 | $this->user = $user; | |
66 | $this->exporttime = time(); | |
67 | $this->id = $CFG->wwwroot . '/portfolio/export/leap2a/' . $this->user->id . '/' . $this->exporttime; | |
68 | ||
69 | $this->dom = new DomDocument('1.0', 'utf-8'); | |
70 | ||
71 | $this->feed = $this->dom->createElement('feed'); | |
72 | $this->feed->setAttribute('xmlns', 'http://www.w3.org/2005/Atom'); | |
73 | $this->feed->setAttribute('xmlns:rdf', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'); | |
74 | $this->feed->setAttribute('xmlns:leap', 'http://wiki.cetis.ac.uk/2009-03/LEAP2A_predicates#'); | |
75 | $this->feed->setAttribute('xmlns:leaptype', 'http://wiki.cetis.ac.uk/2009-03/LEAP2A_types#'); | |
76 | $this->feed->setAttribute('xmlns:categories', 'http://wiki.cetis.ac.uk/2009-03/LEAP2A_categories/'); | |
77 | $this->feed->setAttribute('xmlns:portfolio', $this->id); // this is just a ns for ids of elements for convenience | |
78 | ||
79 | $this->dom->appendChild($this->feed); | |
80 | ||
81 | $this->feed->appendChild($this->dom->createElement('id', $this->id)); | |
8e642e1f | 82 | $this->feed->appendChild($this->dom->createElement('title', get_string('leap2a_feedtitle', 'portfolio', fullname($this->user)))); |
59dd457e PL |
83 | |
84 | $generator = $this->dom->createElement('generator', 'Moodle'); | |
85 | $generator->setAttribute('uri', $CFG->wwwroot); | |
86 | $generator->setAttribute('version', $CFG->version); | |
87 | ||
88 | $this->feed->appendChild($generator); | |
89 | ||
90 | $author = $this->dom->createElement('author'); | |
91 | $author->appendChild($this->dom->createElement('name', fullname($this->user))); | |
92 | $author->appendChild($this->dom->createElement('email', $this->user->email)); | |
93 | $author->appendChild($this->dom->CreateElement('uri', $CFG->wwwroot . '/user/view.php?id=' . $this->user->id)); | |
94 | ||
95 | $this->feed->appendChild($author); | |
96 | // header done, we can start appending entry elements now | |
97 | } | |
98 | ||
99 | /** | |
100 | * adds a entry to the feed ready to be exported | |
101 | * | |
102 | * @param portfolio_format_leap2a_entry $entry the entry to add | |
103 | */ | |
104 | public function add_entry(portfolio_format_leap2a_entry $entry) { | |
105 | if (array_key_exists($entry->id, $this->entries)) { | |
8e642e1f | 106 | throw new portfolio_format_leap2a_exception('leap2a_entryalreadyexists', 'portfolio', '', $entry->id); |
59dd457e PL |
107 | } |
108 | $this->entries[$entry->id] = $entry; | |
109 | return $entry; | |
110 | } | |
111 | ||
112 | /** | |
113 | * make an entry that has previously been added into the feed into a selection. | |
114 | * | |
115 | * @param mixed $selectionentry the entry to make a selection (id or entry object) | |
116 | * @param array $ids array of ids this selection includes | |
117 | * @param string $selectiontype http://wiki.cetis.ac.uk/2009-03/LEAP2A_categories/selection_type | |
118 | */ | |
119 | public function make_selection($selectionentry, $ids, $selectiontype) { | |
120 | $selectionid = null; | |
121 | if ($selectionentry instanceof portfolio_format_leap2a_entry) { | |
122 | $selectionid = $selectionentry->id; | |
123 | } else if (is_string($selectionentry)) { | |
124 | $selectionid = $selectionentry; | |
125 | } | |
126 | if (!array_key_exists($selectionid, $this->entries)) { | |
8e642e1f | 127 | throw new portfolio_format_leap2a_exception('leap2a_invalidentryid', 'portfolio', '', $selectionid); |
59dd457e PL |
128 | } |
129 | foreach ($ids as $entryid) { | |
130 | if (!array_key_exists($entryid, $this->entries)) { | |
8e642e1f | 131 | throw new portfolio_format_leap2a_exception('leap2a_invalidentryid', 'portfolio', '', $entryid); |
59dd457e PL |
132 | } |
133 | $this->entries[$selectionid]->add_link($entryid, 'has_part'); | |
134 | $this->entries[$entryid]->add_link($selectionid, 'is_part_of'); | |
135 | } | |
136 | $this->entries[$selectionid]->add_category($selectiontype, 'selection_type'); | |
137 | if ($this->entries[$selectionid]->type != 'selection') { | |
8e642e1f | 138 | debugging(get_string('leap2a_overwritingselection', 'portfolio', $this->entries[$selectionid]->type)); |
59dd457e PL |
139 | $this->entries[$selectionid]->type = 'selection'; |
140 | } | |
141 | } | |
142 | ||
143 | /** | |
144 | * validate the feed and all entries | |
145 | */ | |
146 | private function validate() { | |
147 | foreach ($this->entries as $entry) { | |
148 | // first call the entry's own validation method | |
149 | // which will throw an exception if there's anything wrong | |
150 | $entry->validate(); | |
151 | // now make sure that all links are in place | |
152 | foreach ($entry->links as $linkedid => $rel) { | |
153 | // the linked to entry exists | |
154 | if (!array_key_exists($linkedid, $this->entries)) { | |
155 | $a = (object)array('rel' => $rel->type, 'to' => $linkedid, 'from' => $entry->id); | |
8e642e1f | 156 | throw new portfolio_format_leap2a_exception('leap2a_nonexistantlink', 'portfolio', '', $a); |
59dd457e PL |
157 | } |
158 | // and contains a link back to us | |
159 | if (!array_key_exists($entry->id, $this->entries[$linkedid]->links)) { | |
160 | ||
161 | } | |
162 | // we could later check that the reltypes were properly inverse, but nevermind for now. | |
163 | } | |
164 | } | |
165 | } | |
166 | ||
167 | /** | |
168 | * return the entire feed as a string | |
169 | * calls validate() first on everything | |
170 | * | |
171 | * @return string | |
172 | */ | |
173 | public function to_xml() { | |
174 | $this->validate(); | |
175 | foreach ($this->entries as $entry) { | |
491fff64 | 176 | $entry->id = 'portfolio:' . $entry->id; |
59dd457e PL |
177 | $this->feed->appendChild($entry->to_dom($this->dom, $this->user)); |
178 | } | |
179 | return $this->dom->saveXML(); | |
180 | } | |
181 | } | |
182 | ||
183 | /** | |
184 | * this class represents a single leap2a entry. | |
185 | * you can create these directly and then add them to the main leap feed object | |
186 | */ | |
187 | class portfolio_format_leap2a_entry { | |
188 | ||
189 | /** entry id - something like forumpost6, must be unique to the feed **/ | |
190 | public $id; | |
191 | /** title of the entry **/ | |
192 | public $title; | |
193 | /** leap2a entry type **/ | |
194 | public $type; | |
195 | /** optional author (only if different to feed author) **/ | |
196 | public $author; | |
197 | /** summary - for split long content **/ | |
198 | public $summary; | |
199 | /** main content of the entry. can be html,text,xhtml or a stored_file **/ | |
200 | public $content; | |
201 | /** updated date - unix timestamp */ | |
202 | public $updated; | |
203 | /** published date (ctime) - unix timestamp */ | |
204 | public $published; | |
205 | ||
206 | /** used internally for file content **/ | |
207 | private $contentsrc; | |
208 | /** used internally for file content **/ | |
209 | private $referencedfile; | |
210 | ||
211 | /** the required fields for a leap2a entry */ | |
212 | private $requiredfields = array( 'id', 'title', 'type'); | |
213 | ||
214 | /** extra fields which usually should be set (except author) but are not required */ | |
215 | private $optionalfields = array('author', 'updated', 'published', 'content', 'summary'); | |
216 | ||
217 | /** links from this entry to other entries */ | |
218 | public $links = array(); | |
219 | ||
220 | /** attachments to this entry */ | |
221 | public $attachments = array(); | |
222 | ||
223 | /** categories for this entry */ | |
224 | private $categories = array(); | |
225 | ||
226 | /** | |
227 | * constructor. All arguments are required (and will be validated) | |
228 | * http://wiki.cetis.ac.uk/2009-03/LEAP2A_types | |
229 | * | |
230 | * @param string $id unique id of this entry. | |
231 | * could be something like forumpost6 for example. | |
232 | * This <b>must</b> be unique to the entire feed. | |
233 | * @param string $title title of the entry. This is pure atom. | |
234 | * @param string $type the leap type of this entry. | |
235 | * @param mixed $content the content of the entry. string (xhtml/html/text) or stored_file | |
236 | */ | |
237 | public function __construct($id, $title, $type, $content=null) { | |
238 | $this->id = $id; | |
239 | $this->title = $title; | |
240 | $this->type = $type; | |
241 | $this->content = $this->__set('content', $content); | |
242 | ||
243 | } | |
244 | ||
245 | /** | |
246 | * override __set to do proper dispatching for different things | |
247 | * only allows the optional and required leap2a entry fields to be set | |
248 | */ | |
249 | public function __set($field, $value) { | |
250 | // detect the case where content is being set to be a file directly | |
251 | if ($field == 'content' && $value instanceof stored_file) { | |
252 | return $this->set_content_file($value); | |
253 | } | |
254 | if (in_array($field, $this->requiredfields) || in_array($field, $this->optionalfields)) { | |
255 | return $this->{$field} = $value; | |
256 | } | |
8e642e1f | 257 | throw new portfolio_format_leap2a_exception('leap2a_invalidentryfield', 'portfolio', '', $field); |
59dd457e PL |
258 | } |
259 | ||
260 | /** | |
261 | * sets the content of this entry to have a source | |
262 | * this will take care of namespacing the filepath | |
263 | * to the final path in the resulting zip file. | |
264 | * | |
265 | * @param stored_file $file the file to link to | |
266 | * @param boolean $overridetype (default true) will set the entry rdf type to resource, | |
267 | * overriding what was previously set. | |
268 | * will be ignored if type is empty already | |
269 | */ | |
270 | public function set_content_file(stored_file $file, $overridetype=true) { | |
271 | $this->contentsrc = portfolio_format_leap2a::get_file_directory() . $file->get_filename(); | |
272 | if (empty($overridetype) || empty($this->type)) { | |
273 | $this->type = 'resource'; | |
274 | } | |
275 | $this->referencedfile = $file; | |
276 | } | |
277 | ||
278 | /** | |
279 | * validate this entry. | |
280 | * at the moment this just makes sure required fields exist | |
281 | * but it could also check things against a list, for example | |
282 | */ | |
283 | public function validate() { | |
284 | foreach ($this->requiredfields as $key) { | |
285 | if (empty($this->{$key})) { | |
8e642e1f | 286 | throw new portfolio_format_leap2a_exception('leap2a_missingfield', 'portfolio', '', $key); |
59dd457e PL |
287 | } |
288 | } | |
289 | if ($this->type == 'selection') { | |
290 | if (count($this->links) == 0) { | |
8e642e1f | 291 | throw new portfolio_format_leap2a_exception('leap2a_emptyselection', 'portfolio'); |
59dd457e PL |
292 | } |
293 | //TODO make sure we have a category with a scheme 'selection_type' | |
294 | } | |
295 | } | |
296 | ||
297 | /** | |
298 | * add a link from this entry to another one | |
299 | * these will be collated at the end of the export (during to_xml) | |
300 | * and validated at that point. This function does no validation | |
301 | * http://wiki.cetis.ac.uk/2009-03/LEAP2A_relationships | |
302 | * | |
303 | * @param mixed $otherentry portfolio_format_leap2a_entry or its id | |
304 | * @param string $reltype (no leap: ns required) | |
305 | * | |
306 | * @return the current entry object. This is so that these calls can be chained | |
307 | * eg $entry->add_link('something6', 'has_part')->add_link('something7', 'has_part'); | |
308 | * | |
309 | */ | |
310 | public function add_link($otherentry, $reltype, $displayorder=null) { | |
311 | if ($otherentry instanceof portfolio_format_leap2a_entry) { | |
312 | $otherentry = $otherentry->id; | |
313 | } | |
314 | if ($otherentry == $this->id) { | |
8e642e1f | 315 | throw new portfolio_format_leap2a_exception('leap2a_selflink', 'portfolio', '', (object)array('rel' => $reltype, 'id' => $this->id)); |
59dd457e PL |
316 | } |
317 | // add on the leap: ns if required | |
318 | if (!in_array($reltype, array('related', 'alternate', 'enclosure'))) { | |
fba249c9 | 319 | $reltype = 'leap:' . $reltype; |
59dd457e PL |
320 | } |
321 | ||
322 | $this->links[$otherentry] = (object)array('rel' => $reltype, 'order' => $displayorder); | |
323 | ||
324 | return $this; | |
325 | } | |
326 | ||
327 | /** | |
328 | * add an attachment to the feed. | |
329 | * adding the file to the files area has to be handled outside this class separately. | |
330 | * | |
331 | * @param stored_file $file the file to add | |
332 | */ | |
333 | public function add_attachment(stored_file $file) { | |
334 | $this->attachments[$file->get_id()] = $file; | |
335 | } | |
336 | ||
337 | /** | |
ed986e00 PL |
338 | * helper function to add a bunch of files at the same time |
339 | * useful for passing $this->multifiles straight through from the portfolio_caller | |
340 | */ | |
341 | public function add_attachments(array $files) { | |
342 | foreach ($files as $file) { | |
343 | $this->add_attachment($file); | |
344 | } | |
345 | } | |
346 | ||
347 | /** | |
59dd457e PL |
348 | * add a category to this entry |
349 | * http://wiki.cetis.ac.uk/2009-03/LEAP2A_categories | |
350 | * | |
351 | * @param string $term eg 'Offline' | |
352 | * @param string $scheme (optional) eg resource_type | |
353 | * @param string $label (optional) eg File | |
354 | * | |
355 | * "tags" should just pass a term here and no scheme or label. | |
356 | * they will be automatically normalised if they have spaces. | |
357 | */ | |
358 | public function add_category($term, $scheme=null, $label=null) { | |
359 | // "normalise" terms and set their label if they have spaces | |
360 | // see http://wiki.cetis.ac.uk/2009-03/LEAP2A_categories#Plain_tags for more information | |
361 | if (empty($scheme) && strpos($term, ' ') !== false) { | |
362 | $label = $term; | |
363 | $term = str_replace(' ', '-', $term); | |
364 | } | |
365 | $this->categories[] = (object)array( | |
366 | 'term' => $term, | |
367 | 'scheme' => $scheme, | |
368 | 'label' => $label, | |
369 | ); | |
370 | } | |
371 | ||
372 | /** | |
373 | * Create an entry element and append all the children | |
374 | * And return it rather than adding it to the dom. | |
375 | * This is handled by the main writer object. | |
376 | * | |
377 | * @param DomDocument $dom use this to create elements | |
378 | * | |
379 | * @return DomElement | |
380 | */ | |
381 | public function to_dom(DomDocument $dom, $feedauthor) { | |
382 | $entry = $dom->createElement('entry'); | |
383 | $entry->appendChild($dom->createElement('id', $this->id)); | |
384 | $entry->appendChild($dom->createElement('title', $this->title)); | |
385 | if ($this->author && $this->author->id != $feedauthor->id) { | |
386 | $author = $dom->createElement('author'); | |
387 | $author->appendChild($dom->createElement('name', fullname($this->author))); | |
388 | $entry->appendChild($author); | |
389 | } | |
390 | // selectively add uncomplicated optional elements | |
391 | foreach (array('updated', 'published') as $field) { | |
392 | if ($this->{$field}) { | |
bce9a819 PL |
393 | $date = date(DATE_ATOM, $this->{$field}); |
394 | $entry->appendChild($dom->createElement($field, $date)); | |
59dd457e PL |
395 | } |
396 | } | |
397 | // deal with referenced files first since it's simple | |
398 | if ($this->contentsrc) { | |
399 | $content = $dom->createElement('content'); | |
400 | $content->setAttribute('src', $this->contentsrc); | |
401 | $content->setAttribute('type', $this->referencedfile->get_mimetype()); | |
402 | $entry->appendChild($content); | |
403 | } else if (empty($this->content)) { | |
404 | $entry->appendChild($dom->createElement('content')); | |
405 | } else { | |
406 | $content = $this->create_xhtmlish_element($dom, 'content', $this->content); | |
407 | $entry->appendChild($content); | |
408 | } | |
409 | ||
410 | if (!empty($this->summary)) { | |
411 | $summary = $this->create_xhtmlish_element($dom, 'summary', $this->summary); | |
412 | $entry->appendChild($summary); | |
413 | } | |
414 | ||
415 | $type = $dom->createElement('rdf:type'); | |
416 | $type->setAttribute('rdf:resource', 'leaptype:' . $this->type); | |
417 | $entry->appendChild($type); | |
418 | ||
419 | foreach ($this->links as $otherentry => $l) { | |
420 | $link = $dom->createElement('link'); | |
421 | $link->setAttribute('rel', $l->rel); | |
491fff64 | 422 | $link->setAttribute('href', 'portfolio:' . $otherentry); |
59dd457e PL |
423 | if ($l->order) { |
424 | $link->setAttribute('leap:display_order', $l->order); | |
425 | } | |
426 | $entry->appendChild($link); | |
427 | } | |
428 | foreach ($this->attachments as $id => $file) { | |
429 | $link = $dom->createElement('link'); | |
430 | $link->setAttribute('rel', 'enclosure'); | |
431 | $link->setAttribute('href', portfolio_format_leap2a::get_file_directory() . $file->get_filename()); | |
432 | $link->setAttribute('length', $file->get_filesize()); | |
433 | $entry->appendChild($link); | |
434 | } | |
435 | foreach ($this->categories as $category) { | |
436 | $cat = $dom->createElement('category'); | |
437 | $cat->setAttribute('term', $category->term); | |
438 | if ($category->scheme) { | |
29fc57af | 439 | $cat->setAttribute('scheme', 'categories:' .$category->scheme . '#'); |
59dd457e PL |
440 | } |
441 | if ($category->label && $category->label != $category->term) { | |
442 | $cat->setAttribute('label', $category->label); | |
443 | } | |
444 | $entry->appendChild($cat); | |
445 | } | |
446 | return $entry; | |
447 | } | |
448 | ||
449 | /** | |
450 | * try to load whatever is in $content into xhtml and add it to the dom. | |
451 | * failing that, load the html, escape it, and set it as the body of the tag | |
452 | * either way it sets the type attribute of the top level element | |
453 | * moodle should always provide xhtml content, but user-defined content can't be trusted | |
454 | * | |
455 | * @param DomDocument $dom the dom doc to use | |
456 | * @param string $tagname usually 'content' or 'summary' | |
457 | * @param string $content the content to use, either xhtml or html. | |
458 | * | |
459 | * @return DomElement | |
460 | */ | |
461 | private function create_xhtmlish_element(DomDocument $dom, $tagname, $content) { | |
462 | $topel = $dom->createElement($tagname); | |
463 | $maybexml = true; | |
464 | if (strpos($content, '<') === false && strpos($content, '>') === false) { | |
465 | $maybexml = false; | |
466 | } | |
467 | // try to load content as xml | |
468 | $tmp = new DomDocument(); | |
469 | if ($maybexml && @$tmp->loadXML('<div>' . $content . '</div>')) { | |
470 | $topel->setAttribute('type', 'xhtml'); | |
471 | $content = $dom->importNode($tmp->documentElement, true); | |
472 | $content->setAttribute('xmlns', 'http://www.w3.org/1999/xhtml'); | |
473 | $topel->appendChild($content); | |
474 | // if that fails, it could still be html | |
475 | } else if ($maybexml && @$tmp->loadHTML($content)) { | |
476 | $topel->setAttribute('type', 'html'); | |
477 | $topel->nodeValue = $content; | |
478 | // TODO figure out how to convert this to xml | |
479 | // TODO because we end up with <html><body> </body></html> wrapped around it | |
480 | // which is annoying | |
481 | // either we already know it's text from the first check | |
482 | // or nothing else has worked anyway | |
483 | } else { | |
484 | $topel->nodeValue = $content; | |
485 | $topel->setAttribute('type', 'text'); | |
486 | return $topel; | |
487 | } | |
488 | return $topel; | |
489 | } | |
490 | } |