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