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