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 LEAP2a writer used by portfolio_format_leap2a 19 * 20 * @package core_portfolio 21 * @copyright 2009 Penny Leach (penny@liip.ch), Martin Dougiamas 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 * 24 */ 25 26defined('MOODLE_INTERNAL') || die(); 27 28/** 29 * Object to encapsulate the writing of leap2a. 30 * 31 * Should be used like: 32 * $writer = portfolio_format_leap2a::leap2a_writer($USER); 33 * $entry = new portfolio_format_leap2a_entry('forumpost6', $title, 'leap2', 'somecontent') 34 * $entry->add_link('something', 'has_part')->add_link('somethingelse', 'has_part'); 35 * .. etc 36 * $writer->add_entry($entry); 37 * $xmlstr = $writer->to_xml(); 38 * 39 * @todo MDL-31287 - find a way to ensure that all referenced files are included 40 * @package core_portfolio 41 * @category portfolio 42 * @copyright 2009 Penny Leach 43 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 44 */ 45class portfolio_format_leap2a_writer { 46 47 /** @var DomDocument the domdocument object used to create elements */ 48 private $dom; 49 50 /** @var DOMElement the top level feed element */ 51 private $feed; 52 53 /** @var stdClass the user exporting data */ 54 private $user; 55 56 /** @var string the id of the feed - this is unique to the user and date and used for portfolio ns as well as feed id */ 57 private $id; 58 59 /** @var array the entries for the feed - keyed on id */ 60 private $entries = array(); 61 62 /** 63 * Constructor - usually generated from portfolio_format_leap2a::leap2a_writer($USER); 64 * 65 * @todo MDL-31302 - add exporter and format 66 * @param stdclass $user the user exporting (almost always $USER) 67 */ 68 public function __construct(stdclass $user) { // todo something else - exporter, format, etc 69 global $CFG; 70 $this->user = $user; 71 $this->exporttime = time(); 72 $this->id = $CFG->wwwroot . '/portfolio/export/leap2a/' . $this->user->id . '/' . $this->exporttime; 73 74 $this->dom = new DomDocument('1.0', 'utf-8'); 75 76 $this->feed = $this->dom->createElement('feed'); 77 $this->feed->setAttribute('xmlns', 'http://www.w3.org/2005/Atom'); 78 $this->feed->setAttribute('xmlns:rdf', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'); 79 $this->feed->setAttribute('xmlns:leap2', 'http://terms.leapspecs.org/'); 80 $this->feed->setAttribute('xmlns:categories', 'http://wiki.leapspecs.org/2A/categories'); 81 $this->feed->setAttribute('xmlns:portfolio', $this->id); // this is just a ns for ids of elements for convenience 82 83 $this->dom->appendChild($this->feed); 84 85 $this->feed->appendChild($this->dom->createElement('id', $this->id)); 86 $this->feed->appendChild($this->dom->createElement('title', get_string('leap2a_feedtitle', 'portfolio', fullname($this->user)))); 87 $this->feed->appendChild($this->dom->createElement('leap2:version', 'http://www.leapspecs.org/2010-07/2A/')); 88 89 90 $generator = $this->dom->createElement('generator', 'Moodle'); 91 $generator->setAttribute('uri', $CFG->wwwroot); 92 $generator->setAttribute('version', $CFG->version); 93 94 $this->feed->appendChild($generator); 95 96 $author = $this->dom->createElement('author'); 97 $author->appendChild($this->dom->createElement('name', fullname($this->user))); 98 $author->appendChild($this->dom->createElement('email', $this->user->email)); 99 $author->appendChild($this->dom->CreateElement('uri', $CFG->wwwroot . '/user/view.php?id=' . $this->user->id)); 100 101 $this->feed->appendChild($author); 102 // header done, we can start appending entry elements now 103 } 104 105 /** 106 * Adds a entry to the feed ready to be exported 107 * 108 * @param portfolio_format_leap2a_entry $entry new feed entry to add 109 * @return portfolio_format_leap2a_entry 110 */ 111 public function add_entry(portfolio_format_leap2a_entry $entry) { 112 if (array_key_exists($entry->id, $this->entries)) { 113 if (!($entry instanceof portfolio_format_leap2a_file)) { 114 throw new portfolio_format_leap2a_exception('leap2a_entryalreadyexists', 'portfolio', '', $entry->id); 115 } 116 } 117 $this->entries[$entry->id] = $entry; 118 return $entry; 119 } 120 121 /** 122 * Select an entry that has previously been added into the feed 123 * 124 * @param portfolio_format_leap2a_entry|string $selectionentry the entry to make a selection (id or entry object) 125 * @param array $ids array of ids this selection includes 126 * @param string $selectiontype for selection type, see: http://wiki.cetis.ac.uk/2009-03/LEAP2A_categories/selection_type 127 */ 128 public function make_selection($selectionentry, $ids, $selectiontype) { 129 $selectionid = null; 130 if ($selectionentry instanceof portfolio_format_leap2a_entry) { 131 $selectionid = $selectionentry->id; 132 } else if (is_string($selectionentry)) { 133 $selectionid = $selectionentry; 134 } 135 if (!array_key_exists($selectionid, $this->entries)) { 136 throw new portfolio_format_leap2a_exception('leap2a_invalidentryid', 'portfolio', '', $selectionid); 137 } 138 foreach ($ids as $entryid) { 139 if (!array_key_exists($entryid, $this->entries)) { 140 throw new portfolio_format_leap2a_exception('leap2a_invalidentryid', 'portfolio', '', $entryid); 141 } 142 $this->entries[$selectionid]->add_link($entryid, 'has_part'); 143 $this->entries[$entryid]->add_link($selectionid, 'is_part_of'); 144 } 145 $this->entries[$selectionid]->add_category($selectiontype, 'selection_type'); 146 if ($this->entries[$selectionid]->type != 'selection') { 147 debugging(get_string('leap2a_overwritingselection', 'portfolio', $this->entries[$selectionid]->type)); 148 $this->entries[$selectionid]->type = 'selection'; 149 } 150 } 151 152 /** 153 * Helper function to link some stored_files into the feed and link them to a particular entry 154 * 155 * @param portfolio_format_leap2a_entry $entry feed object 156 * @param array $files array of stored_files to link 157 */ 158 public function link_files($entry, $files) { 159 foreach ($files as $file) { 160 $fileentry = new portfolio_format_leap2a_file($file->get_filename(), $file); 161 $this->add_entry($fileentry); 162 $entry->add_link($fileentry, 'related'); 163 $fileentry->add_link($entry, 'related'); 164 } 165 } 166 167 /** 168 * Validate the feed and all entries 169 */ 170 private function validate() { 171 foreach ($this->entries as $entry) { 172 // first call the entry's own validation method 173 // which will throw an exception if there's anything wrong 174 $entry->validate(); 175 // now make sure that all links are in place 176 foreach ($entry->links as $linkedid => $rel) { 177 // the linked to entry exists 178 if (!array_key_exists($linkedid, $this->entries)) { 179 $a = (object)array('rel' => $rel->type, 'to' => $linkedid, 'from' => $entry->id); 180 throw new portfolio_format_leap2a_exception('leap2a_nonexistantlink', 'portfolio', '', $a); 181 } 182 // and contains a link back to us 183 if (!array_key_exists($entry->id, $this->entries[$linkedid]->links)) { 184 185 } 186 // we could later check that the reltypes were properly inverse, but nevermind for now. 187 } 188 } 189 } 190 191 /** 192 * Return the entire feed as a string. 193 * Then, it calls for validation 194 * 195 * @return string feeds' content in xml 196 */ 197 public function to_xml() { 198 $this->validate(); 199 foreach ($this->entries as $entry) { 200 $entry->id = 'portfolio:' . $entry->id; 201 $this->feed->appendChild($entry->to_dom($this->dom, $this->user)); 202 } 203 return $this->dom->saveXML(); 204 } 205} 206 207/** 208 * This class represents a single leap2a entry. 209 * 210 * You can create these directly and then add them to the main leap feed object 211 * 212 * @package core_portfolio 213 * @category portfolio 214 * @copyright 2009 Penny Leach 215 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 216 */ 217class portfolio_format_leap2a_entry { 218 219 /** @var string entry id - something like forumpost6, must be unique to the feed */ 220 public $id; 221 222 /** @var string title of the entry */ 223 public $title; 224 225 /** @var string leap2a entry type */ 226 public $type; 227 228 /** @var string optional author (only if different to feed author) */ 229 public $author; 230 231 /** @var string summary - for split long content */ 232 public $summary; 233 234 /** @var mixed main content of the entry. can be html,text,or xhtml. for a stored_file, use portfolio_format_leap2a_file **/ 235 public $content; 236 237 /** @var int updated date - unix timestamp */ 238 public $updated; 239 240 /** @var int published date (ctime) - unix timestamp */ 241 public $published; 242 243 /** @var array the required fields for a leap2a entry */ 244 private $requiredfields = array( 'id', 'title', 'type'); 245 246 /** @var array extra fields which usually should be set (except author) but are not required */ 247 private $optionalfields = array('author', 'updated', 'published', 'content', 'summary'); 248 249 /** @var array links from this entry to other entries */ 250 public $links = array(); 251 252 /** @var array attachments to this entry */ 253 public $attachments = array(); 254 255 /** @var array categories for this entry */ 256 private $categories = array(); 257 258 /** 259 * Constructor. All arguments are required (and will be validated) 260 * http://wiki.cetis.ac.uk/2009-03/LEAP2A_types 261 * 262 * @param string $id unique id of this entry. 263 * could be something like forumpost6 for example. 264 * This <b>must</b> be unique to the entire feed. 265 * @param string $title title of the entry. This is pure atom. 266 * @param string $type the leap type of this entry. 267 * @param mixed $content the content of the entry. string (xhtml/html/text) 268 */ 269 public function __construct($id, $title, $type, $content=null) { 270 $this->id = $id; 271 $this->title = $title; 272 $this->type = $type; 273 $this->content = $this->__set('content', $content); 274 275 } 276 277 /** 278 * Override __set to do proper dispatching for different things. 279 * Only allows the optional and required leap2a entry fields to be set 280 * 281 * @param string $field property's name 282 * @param mixed $value property's value 283 * @return mixed 284 */ 285 public function __set($field, $value) { 286 // detect the case where content is being set to be a file directly 287 if ($field == 'content' && $value instanceof stored_file) { 288 throw new portfolio_format_leap2a_exception('leap2a_filecontent', 'portfolio'); 289 } 290 if (in_array($field, $this->requiredfields) || in_array($field, $this->optionalfields)) { 291 return $this->{$field} = $value; 292 } 293 throw new portfolio_format_leap2a_exception('leap2a_invalidentryfield', 'portfolio', '', $field); 294 } 295 296 297 /** 298 * Validate this entry. 299 * At the moment this just makes sure required fields exist 300 * but it could also check things against a list, for example 301 * 302 * @todo MDL-31303 - add category with a scheme 'selection_type' 303 */ 304 public function validate() { 305 foreach ($this->requiredfields as $key) { 306 if (empty($this->{$key})) { 307 throw new portfolio_format_leap2a_exception('leap2a_missingfield', 'portfolio', '', $key); 308 } 309 } 310 if ($this->type == 'selection') { 311 if (count($this->links) == 0) { 312 throw new portfolio_format_leap2a_exception('leap2a_emptyselection', 'portfolio'); 313 } 314 //TODO make sure we have a category with a scheme 'selection_type' 315 } 316 } 317 318 /** 319 * Add a link from this entry to another one. 320 * These will be collated at the end of the export (during to_xml) 321 * and validated at that point. This function does no validation 322 * {@link http://wiki.cetis.ac.uk/2009-03/LEAP2A_relationships} 323 * 324 * @param portfolio_format_leap2a_entry|string $otherentry portfolio_format_leap2a_entry or its id 325 * @param string $reltype (no leap2: ns required) 326 * @param string $displayorder (optional) 327 * @return portfolio_format_leap2a_entry the current entry object. This is so that these calls can be chained 328 * eg $entry->add_link('something6', 'has_part')->add_link('something7', 329 * 'has_part'); 330 */ 331 public function add_link($otherentry, $reltype, $displayorder=null) { 332 if ($otherentry instanceof portfolio_format_leap2a_entry) { 333 $otherentry = $otherentry->id; 334 } 335 if ($otherentry == $this->id) { 336 throw new portfolio_format_leap2a_exception('leap2a_selflink', 'portfolio', '', (object)array('rel' => $reltype, 'id' => $this->id)); 337 } 338 // add on the leap2: ns if required 339 if (!in_array($reltype, array('related', 'alternate', 'enclosure'))) { 340 $reltype = 'leap2:' . $reltype; 341 } 342 343 $this->links[$otherentry] = (object)array('rel' => $reltype, 'order' => $displayorder); 344 345 return $this; 346 } 347 348 /** 349 * Add a category to this entry 350 * {@link http://wiki.cetis.ac.uk/2009-03/LEAP2A_categories} 351 * "tags" should just pass a term here and no scheme or label. 352 * They will be automatically normalised if they have spaces. 353 * 354 * @param string $term eg 'Offline' 355 * @param string $scheme (optional) eg resource_type 356 * @param string $label (optional) eg File 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 * @param stdClass $feedauthor object of author(user) info 379 * @return DOMDocument 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}) { 393 $date = date(DATE_ATOM, $this->{$field}); 394 $entry->appendChild($dom->createElement($field, $date)); 395 } 396 } 397 if (empty($this->content)) { 398 $entry->appendChild($dom->createElement('content')); 399 } else { 400 $content = $this->create_xhtmlish_element($dom, 'content', $this->content); 401 $entry->appendChild($content); 402 } 403 404 if (!empty($this->summary)) { 405 $summary = $this->create_xhtmlish_element($dom, 'summary', $this->summary); 406 $entry->appendChild($summary); 407 } 408 409 $type = $dom->createElement('rdf:type'); 410 $type->setAttribute('rdf:resource', 'leap2:' . $this->type); 411 $entry->appendChild($type); 412 413 foreach ($this->links as $otherentry => $l) { 414 $link = $dom->createElement('link'); 415 $link->setAttribute('rel', $l->rel); 416 $link->setAttribute('href', 'portfolio:' . $otherentry); 417 if ($l->order) { 418 $link->setAttribute('leap2:display_order', $l->order); 419 } 420 $entry->appendChild($link); 421 } 422 423 $this->add_extra_links($dom, $entry); // hook for subclass 424 425 foreach ($this->categories as $category) { 426 $cat = $dom->createElement('category'); 427 $cat->setAttribute('term', $category->term); 428 if ($category->scheme) { 429 $cat->setAttribute('scheme', 'categories:' .$category->scheme . '#'); 430 } 431 if ($category->label && $category->label != $category->term) { 432 $cat->setAttribute('label', $category->label); 433 } 434 $entry->appendChild($cat); 435 } 436 return $entry; 437 } 438 439 /** 440 * Try to load whatever is in $content into xhtml and add it to the dom. 441 * Failing that, load the html, escape it, and set it as the body of the tag. 442 * Either way it sets the type attribute of the top level element. 443 * Moodle should always provide xhtml content, but user-defined content can't be trusted 444 * 445 * @todo MDL-31304 - convert <html><body> </body></html> to xml 446 * @param DomDocument $dom the dom doc to use 447 * @param string $tagname usually 'content' or 'summary' 448 * @param string $content the content to use, either xhtml or html. 449 * @return DomDocument 450 */ 451 private function create_xhtmlish_element(DomDocument $dom, $tagname, $content) { 452 $topel = $dom->createElement($tagname); 453 $maybexml = true; 454 if (strpos($content, '<') === false && strpos($content, '>') === false) { 455 $maybexml = false; 456 } 457 // try to load content as xml 458 $tmp = new DomDocument(); 459 if ($maybexml && @$tmp->loadXML('<div>' . $content . '</div>')) { 460 $topel->setAttribute('type', 'xhtml'); 461 $content = $dom->importNode($tmp->documentElement, true); 462 $content->setAttribute('xmlns', 'http://www.w3.org/1999/xhtml'); 463 $topel->appendChild($content); 464 // if that fails, it could still be html 465 } else if ($maybexml && @$tmp->loadHTML($content)) { 466 $topel->setAttribute('type', 'html'); 467 $topel->nodeValue = $content; 468 // TODO figure out how to convert this to xml 469 // TODO because we end up with <html><body> </body></html> wrapped around it 470 // which is annoying 471 // either we already know it's text from the first check 472 // or nothing else has worked anyway 473 } else { 474 $topel->nodeValue = $content; 475 $topel->setAttribute('type', 'text'); 476 return $topel; 477 } 478 return $topel; 479 } 480 481 /** 482 * Hook function for subclasses to add extra links (like for files) 483 * 484 * @param DomDocument $dom feed object 485 * @param DomDocument $entry feed added link 486 */ 487 protected function add_extra_links($dom, $entry) {} 488} 489 490/** 491 * Subclass of entry, purely for dealing with files 492 * 493 * @package core_portfolio 494 * @category portfolio 495 * @copyright 2009 Penny Leach 496 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 497 */ 498class portfolio_format_leap2a_file extends portfolio_format_leap2a_entry { 499 500 /** @var file_stored for the dealing file */ 501 protected $referencedfile; 502 503 /** 504 * Overridden constructor to set up the file. 505 * 506 * @param string $title title of the entry 507 * @param stored_file $file file storage instance 508 */ 509 public function __construct($title, stored_file $file) { 510 $id = portfolio_format_leap2a::file_id_prefix() . $file->get_id(); 511 parent::__construct($id, $title, 'resource'); 512 $this->referencedfile = $file; 513 $this->published = $this->referencedfile->get_timecreated(); 514 $this->updated = $this->referencedfile->get_timemodified(); 515 $this->add_category('offline', 'resource_type'); 516 } 517 518 /** 519 * Implement the hook to add extra links to attach the file in an enclosure 520 * 521 * @param DomDocument $dom feed object 522 * @param DomDocument $entry feed added link 523 */ 524 protected function add_extra_links($dom, $entry) { 525 $link = $dom->createElement('link'); 526 $link->setAttribute('rel', 'enclosure'); 527 $link->setAttribute('href', portfolio_format_leap2a::get_file_directory() . $this->referencedfile->get_filename()); 528 $link->setAttribute('length', $this->referencedfile->get_filesize()); 529 $link->setAttribute('type', $this->referencedfile->get_mimetype()); 530 $entry->appendChild($link); 531 } 532} 533 534