See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401]
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 * Library to handle drag and drop course uploads 19 * 20 * @package core 21 * @subpackage lib 22 * @copyright 2012 Davo smith 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 26 defined('MOODLE_INTERNAL') || die(); 27 28 require_once($CFG->dirroot.'/repository/lib.php'); 29 require_once($CFG->dirroot.'/repository/upload/lib.php'); 30 require_once($CFG->dirroot.'/course/lib.php'); 31 32 /** 33 * Add the Javascript to enable drag and drop upload to a course page 34 * 35 * @param object $course The currently displayed course 36 * @param array $modnames The list of enabled (visible) modules on this site 37 * @return void 38 */ 39 function dndupload_add_to_course($course, $modnames) { 40 global $CFG, $PAGE; 41 42 $showstatus = optional_param('notifyeditingon', false, PARAM_BOOL); 43 44 // Get all handlers. 45 $handler = new dndupload_handler($course, $modnames); 46 $jsdata = $handler->get_js_data(); 47 if (empty($jsdata->types) && empty($jsdata->filehandlers)) { 48 return; // No valid handlers - don't enable drag and drop. 49 } 50 51 // Add the javascript to the page. 52 $jsmodule = array( 53 'name' => 'coursedndupload', 54 'fullpath' => '/course/dndupload.js', 55 'strings' => array( 56 array('addfilehere', 'moodle'), 57 array('dndworkingfiletextlink', 'moodle'), 58 array('dndworkingfilelink', 'moodle'), 59 array('dndworkingfiletext', 'moodle'), 60 array('dndworkingfile', 'moodle'), 61 array('dndworkingtextlink', 'moodle'), 62 array('dndworkingtext', 'moodle'), 63 array('dndworkinglink', 'moodle'), 64 array('namedfiletoolarge', 'moodle'), 65 array('actionchoice', 'moodle'), 66 array('servererror', 'moodle'), 67 array('filereaderror', 'moodle'), 68 array('upload', 'moodle'), 69 array('cancel', 'moodle'), 70 array('changesmadereallygoaway', 'moodle') 71 ), 72 'requires' => array('node', 'event', 'json', 'anim') 73 ); 74 $vars = array( 75 array('courseid' => $course->id, 76 'maxbytes' => get_user_max_upload_file_size($PAGE->context, $CFG->maxbytes, $course->maxbytes), 77 'handlers' => $handler->get_js_data(), 78 'showstatus' => $showstatus) 79 ); 80 81 $PAGE->requires->js_init_call('M.course_dndupload.init', $vars, true, $jsmodule); 82 } 83 84 85 /** 86 * Stores all the information about the available dndupload handlers 87 * 88 * @package core 89 * @copyright 2012 Davo Smith 90 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 91 */ 92 class dndupload_handler { 93 94 /** 95 * @var array A list of all registered mime types that can be dropped onto a course 96 * along with the modules that will handle them. 97 */ 98 protected $types = array(); 99 100 /** 101 * @var array A list of the different file types (extensions) that different modules 102 * will handle. 103 */ 104 protected $filehandlers = array(); 105 106 /** 107 * @var context_course|null 108 */ 109 protected $context = null; 110 111 /** 112 * Gather a list of dndupload handlers from the different mods 113 * 114 * @param object $course The course this is being added to (to check course_allowed_module() ) 115 */ 116 public function __construct($course, $modnames = null) { 117 global $CFG, $PAGE; 118 119 // Add some default types to handle. 120 // Note: 'Files' type is hard-coded into the Javascript as this needs to be ... 121 // ... treated a little differently. 122 $this->register_type('url', array('url', 'text/uri-list', 'text/x-moz-url'), get_string('addlinkhere', 'moodle'), 123 get_string('nameforlink', 'moodle'), get_string('whatforlink', 'moodle'), 10); 124 $this->register_type('text/html', array('text/html'), get_string('addpagehere', 'moodle'), 125 get_string('nameforpage', 'moodle'), get_string('whatforpage', 'moodle'), 20); 126 $this->register_type('text', array('text', 'text/plain'), get_string('addpagehere', 'moodle'), 127 get_string('nameforpage', 'moodle'), get_string('whatforpage', 'moodle'), 30); 128 129 $this->context = context_course::instance($course->id); 130 131 // Loop through all modules to find handlers. 132 $mods = get_plugin_list_with_function('mod', 'dndupload_register'); 133 foreach ($mods as $component => $funcname) { 134 list($modtype, $modname) = core_component::normalize_component($component); 135 if ($modnames && !array_key_exists($modname, $modnames)) { 136 continue; // Module is deactivated (hidden) at the site level. 137 } 138 if (!course_allowed_module($course, $modname)) { 139 continue; // User does not have permission to add this module to the course. 140 } 141 $resp = $funcname(); 142 if (!$resp) { 143 continue; 144 } 145 if (isset($resp['files'])) { 146 foreach ($resp['files'] as $file) { 147 $this->register_file_handler($file['extension'], $modname, $file['message']); 148 } 149 } 150 if (isset($resp['addtypes'])) { 151 foreach ($resp['addtypes'] as $type) { 152 if (isset($type['priority'])) { 153 $priority = $type['priority']; 154 } else { 155 $priority = 100; 156 } 157 if (!isset($type['handlermessage'])) { 158 $type['handlermessage'] = ''; 159 } 160 $this->register_type($type['identifier'], $type['datatransfertypes'], 161 $type['addmessage'], $type['namemessage'], $type['handlermessage'], $priority); 162 } 163 } 164 if (isset($resp['types'])) { 165 foreach ($resp['types'] as $type) { 166 $noname = !empty($type['noname']); 167 $this->register_type_handler($type['identifier'], $modname, $type['message'], $noname); 168 } 169 } 170 $PAGE->requires->string_for_js('pluginname', $modname); 171 } 172 } 173 174 /** 175 * Used to add a new mime type that can be drag and dropped onto a 176 * course displayed in a browser window 177 * 178 * @param string $identifier The name that this type will be known as 179 * @param array $datatransfertypes An array of the different types in the browser 180 * 'dataTransfer.types' object that will map to this type 181 * @param string $addmessage The message to display in the browser when this type is being 182 * dragged onto the page 183 * @param string $namemessage The message to pop up when asking for the name to give the 184 * course module instance when it is created 185 * @param string $handlermessage The message to pop up when asking which module should handle this type 186 * @param int $priority Controls the order in which types are checked by the browser (mainly 187 * needed to check for 'text' last as that is usually given as fallback) 188 */ 189 protected function register_type($identifier, $datatransfertypes, $addmessage, $namemessage, $handlermessage, $priority=100) { 190 if ($this->is_known_type($identifier)) { 191 throw new coding_exception("Type $identifier is already registered"); 192 } 193 194 $add = new stdClass; 195 $add->identifier = $identifier; 196 $add->datatransfertypes = $datatransfertypes; 197 $add->addmessage = $addmessage; 198 $add->namemessage = $namemessage; 199 $add->handlermessage = $handlermessage; 200 $add->priority = $priority; 201 $add->handlers = array(); 202 203 $this->types[$identifier] = $add; 204 } 205 206 /** 207 * Used to declare that a particular module will handle a particular type 208 * of dropped data 209 * 210 * @param string $type The name of the type (as declared in register_type) 211 * @param string $module The name of the module to handle this type 212 * @param string $message The message to show the user if more than one handler is registered 213 * for a type and the user needs to make a choice between them 214 * @param bool $noname If true, the 'name' dialog should be disabled in the pop-up. 215 * @throws coding_exception 216 */ 217 protected function register_type_handler($type, $module, $message, $noname) { 218 if (!$this->is_known_type($type)) { 219 throw new coding_exception("Trying to add handler for unknown type $type"); 220 } 221 222 $add = new stdClass; 223 $add->type = $type; 224 $add->module = $module; 225 $add->message = $message; 226 $add->noname = $noname ? 1 : 0; 227 228 $this->types[$type]->handlers[] = $add; 229 } 230 231 /** 232 * Used to declare that a particular module will handle a particular type 233 * of dropped file 234 * 235 * @param string $extension The file extension to handle ('*' for all types) 236 * @param string $module The name of the module to handle this type 237 * @param string $message The message to show the user if more than one handler is registered 238 * for a type and the user needs to make a choice between them 239 */ 240 protected function register_file_handler($extension, $module, $message) { 241 $extension = strtolower($extension); 242 243 $add = new stdClass; 244 $add->extension = $extension; 245 $add->module = $module; 246 $add->message = $message; 247 248 $this->filehandlers[] = $add; 249 } 250 251 /** 252 * Check to see if the type has been registered 253 * 254 * @param string $type The identifier of the type you are interested in 255 * @return bool True if the type is registered 256 */ 257 public function is_known_type($type) { 258 return array_key_exists($type, $this->types); 259 } 260 261 /** 262 * Check to see if the module in question has registered to handle the 263 * type given 264 * 265 * @param string $module The name of the module 266 * @param string $type The identifier of the type 267 * @return bool True if the module has registered to handle that type 268 */ 269 public function has_type_handler($module, $type) { 270 if (!$this->is_known_type($type)) { 271 throw new coding_exception("Checking for handler for unknown type $type"); 272 } 273 foreach ($this->types[$type]->handlers as $handler) { 274 if ($handler->module == $module) { 275 return true; 276 } 277 } 278 return false; 279 } 280 281 /** 282 * Check to see if the module in question has registered to handle files 283 * with the given extension (or to handle all file types) 284 * 285 * @param string $module The name of the module 286 * @param string $extension The extension of the uploaded file 287 * @return bool True if the module has registered to handle files with 288 * that extension (or to handle all file types) 289 */ 290 public function has_file_handler($module, $extension) { 291 foreach ($this->filehandlers as $handler) { 292 if ($handler->module == $module) { 293 if ($handler->extension == '*' || $handler->extension == $extension) { 294 return true; 295 } 296 } 297 } 298 return false; 299 } 300 301 /** 302 * Gets a list of the file types that are handled by a particular module 303 * 304 * @param string $module The name of the module to check 305 * @return array of file extensions or string '*' 306 */ 307 public function get_handled_file_types($module) { 308 $types = array(); 309 foreach ($this->filehandlers as $handler) { 310 if ($handler->module == $module) { 311 if ($handler->extension == '*') { 312 return '*'; 313 } else { 314 // Prepending '.' as otherwise mimeinfo fails. 315 $types[] = '.'.$handler->extension; 316 } 317 } 318 } 319 return $types; 320 } 321 322 /** 323 * Returns an object to pass onto the javascript code with data about all the 324 * registered file / type handlers 325 * 326 * @return object Data to pass on to Javascript code 327 */ 328 public function get_js_data() { 329 global $CFG; 330 331 $ret = new stdClass; 332 333 // Sort the types by priority. 334 uasort($this->types, array($this, 'type_compare')); 335 336 $ret->types = array(); 337 if (!empty($CFG->dndallowtextandlinks)) { 338 foreach ($this->types as $type) { 339 if (empty($type->handlers)) { 340 continue; // Skip any types without registered handlers. 341 } 342 $ret->types[] = $type; 343 } 344 } 345 346 $ret->filehandlers = $this->filehandlers; 347 $uploadrepo = repository::get_instances(array('type' => 'upload', 'currentcontext' => $this->context)); 348 if (empty($uploadrepo)) { 349 $ret->filehandlers = array(); // No upload repo => no file handlers. 350 } 351 352 return $ret; 353 } 354 355 /** 356 * Comparison function used when sorting types by priority 357 * @param object $type1 first type to compare 358 * @param object $type2 second type to compare 359 * @return integer -1 for $type1 < $type2; 1 for $type1 > $type2; 0 for equal 360 */ 361 protected function type_compare($type1, $type2) { 362 if ($type1->priority < $type2->priority) { 363 return -1; 364 } 365 if ($type1->priority > $type2->priority) { 366 return 1; 367 } 368 return 0; 369 } 370 371 } 372 373 /** 374 * Processes the upload, creating the course module and returning the result 375 * 376 * @package core 377 * @copyright 2012 Davo Smith 378 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 379 */ 380 class dndupload_ajax_processor { 381 382 /** Returned when no error has occurred */ 383 const ERROR_OK = 0; 384 385 /** @var object The course that we are uploading to */ 386 protected $course = null; 387 388 /** @var context_course The course context for capability checking */ 389 protected $context = null; 390 391 /** @var int The section number we are uploading to */ 392 protected $section = null; 393 394 /** @var string The type of upload (e.g. 'Files', 'text/plain') */ 395 protected $type = null; 396 397 /** @var object The details of the module type that will be created */ 398 protected $module= null; 399 400 /** @var object The course module that has been created */ 401 protected $cm = null; 402 403 /** @var dndupload_handler used to check the allowed file types */ 404 protected $dnduploadhandler = null; 405 406 /** @var string The name to give the new activity instance */ 407 protected $displayname = null; 408 409 /** 410 * Set up some basic information needed to handle the upload 411 * 412 * @param int $courseid The ID of the course we are uploading to 413 * @param int $section The section number we are uploading to 414 * @param string $type The type of upload (as reported by the browser) 415 * @param string $modulename The name of the module requested to handle this upload 416 */ 417 public function __construct($courseid, $section, $type, $modulename) { 418 global $DB; 419 420 if (!defined('AJAX_SCRIPT')) { 421 throw new coding_exception('dndupload_ajax_processor should only be used within AJAX requests'); 422 } 423 424 $this->course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST); 425 426 require_login($this->course, false); 427 $this->context = context_course::instance($this->course->id); 428 429 if (!is_number($section) || $section < 0) { 430 throw new coding_exception("Invalid section number $section"); 431 } 432 $this->section = $section; 433 $this->type = $type; 434 435 if (!$this->module = $DB->get_record('modules', array('name' => $modulename))) { 436 throw new coding_exception("Module $modulename does not exist"); 437 } 438 439 $this->dnduploadhandler = new dndupload_handler($this->course); 440 } 441 442 /** 443 * Check if this upload is a 'file' upload 444 * 445 * @return bool true if it is a 'file' upload, false otherwise 446 */ 447 protected function is_file_upload() { 448 return ($this->type == 'Files'); 449 } 450 451 /** 452 * Process the upload - creating the module in the course and returning the result to the browser 453 * 454 * @param string $displayname optional the name (from the browser) to give the course module instance 455 * @param string $content optional the content of the upload (for non-file uploads) 456 */ 457 public function process($displayname = null, $content = null) { 458 require_capability('moodle/course:manageactivities', $this->context); 459 460 if ($this->is_file_upload()) { 461 require_capability('moodle/course:managefiles', $this->context); 462 if ($content != null) { 463 throw new moodle_exception('fileuploadwithcontent', 'moodle'); 464 } 465 } else { 466 if (empty($content)) { 467 throw new moodle_exception('dnduploadwithoutcontent', 'moodle'); 468 } 469 } 470 471 require_sesskey(); 472 473 $this->displayname = $displayname; 474 475 if ($this->is_file_upload()) { 476 $this->handle_file_upload(); 477 } else { 478 $this->handle_other_upload($content); 479 } 480 } 481 482 /** 483 * Handle uploads containing files - create the course module, ask the upload repository 484 * to process the file, ask the mod to set itself up, then return the result to the browser 485 */ 486 protected function handle_file_upload() { 487 global $CFG; 488 489 // Add the file to a draft file area. 490 $draftitemid = file_get_unused_draft_itemid(); 491 $maxbytes = get_user_max_upload_file_size($this->context, $CFG->maxbytes, $this->course->maxbytes); 492 $types = $this->dnduploadhandler->get_handled_file_types($this->module->name); 493 $repo = repository::get_instances(array('type' => 'upload', 'currentcontext' => $this->context)); 494 if (empty($repo)) { 495 throw new moodle_exception('errornouploadrepo', 'moodle'); 496 } 497 $repo = reset($repo); // Get the first (and only) upload repo. 498 // Pre-emptively purge the navigation cache so the upload repo can close the session. 499 navigation_cache::destroy_volatile_caches(); 500 $details = $repo->process_upload(null, $maxbytes, $types, '/', $draftitemid); 501 if (empty($this->displayname)) { 502 $this->displayname = $this->display_name_from_file($details['file']); 503 } 504 505 // Create a course module to hold the new instance. 506 $this->create_course_module(); 507 508 // Ask the module to set itself up. 509 $moduledata = $this->prepare_module_data($draftitemid); 510 $instanceid = plugin_callback('mod', $this->module->name, 'dndupload', 'handle', array($moduledata), 'invalidfunction'); 511 if ($instanceid === 'invalidfunction') { 512 throw new coding_exception("{$this->module->name} does not support drag and drop upload (missing {$this->module->name}_dndupload_handle function"); 513 } 514 515 // Finish setting up the course module. 516 $this->finish_setup_course_module($instanceid); 517 } 518 519 /** 520 * Handle uploads not containing file - create the course module, ask the mod to 521 * set itself up, then return the result to the browser 522 * 523 * @param string $content the content uploaded to the browser 524 */ 525 protected function handle_other_upload($content) { 526 // Check this plugin is registered to handle this type of upload 527 if (!$this->dnduploadhandler->has_type_handler($this->module->name, $this->type)) { 528 $info = (object)array('modname' => $this->module->name, 'type' => $this->type); 529 throw new moodle_exception('moddoesnotsupporttype', 'moodle', $info); 530 } 531 532 // Create a course module to hold the new instance. 533 $this->create_course_module(); 534 535 // Ask the module to set itself up. 536 $moduledata = $this->prepare_module_data(null, $content); 537 $instanceid = plugin_callback('mod', $this->module->name, 'dndupload', 'handle', array($moduledata), 'invalidfunction'); 538 if ($instanceid === 'invalidfunction') { 539 throw new coding_exception("{$this->module->name} does not support drag and drop upload (missing {$this->module->name}_dndupload_handle function"); 540 } 541 542 // Finish setting up the course module. 543 $this->finish_setup_course_module($instanceid); 544 } 545 546 /** 547 * Generate the name of the mod instance from the name of the file 548 * (remove the extension and convert underscore => space 549 * 550 * @param string $filename the filename of the uploaded file 551 * @return string the display name to use 552 */ 553 protected function display_name_from_file($filename) { 554 $pos = core_text::strrpos($filename, '.'); 555 if ($pos) { // Want to skip if $pos === 0 OR $pos === false. 556 $filename = core_text::substr($filename, 0, $pos); 557 } 558 return str_replace('_', ' ', $filename); 559 } 560 561 /** 562 * Create the coursemodule to hold the file/content that has been uploaded 563 */ 564 protected function create_course_module() { 565 global $CFG; 566 require_once($CFG->dirroot.'/course/modlib.php'); 567 list($module, $context, $cw, $cm, $data) = prepare_new_moduleinfo_data($this->course, $this->module->name, $this->section); 568 569 $data->coursemodule = $data->id = add_course_module($data); 570 $this->cm = $data; 571 } 572 573 /** 574 * Gather together all the details to pass on to the mod, so that it can initialise it's 575 * own database tables 576 * 577 * @param int $draftitemid optional the id of the draft area containing the file (for file uploads) 578 * @param string $content optional the content dropped onto the course (for non-file uploads) 579 * @return object data to pass on to the mod, containing: 580 * string $type the 'type' as registered with dndupload_handler (or 'Files') 581 * object $course the course the upload was for 582 * int $draftitemid optional the id of the draft area containing the files 583 * int $coursemodule id of the course module that has already been created 584 * string $displayname the name to use for this activity (can be overriden by the mod) 585 */ 586 protected function prepare_module_data($draftitemid = null, $content = null) { 587 $data = new stdClass(); 588 $data->type = $this->type; 589 $data->course = $this->course; 590 if ($draftitemid) { 591 $data->draftitemid = $draftitemid; 592 } else if ($content) { 593 $data->content = $content; 594 } 595 $data->coursemodule = $this->cm->id; 596 $data->displayname = $this->displayname; 597 return $data; 598 } 599 600 /** 601 * Called after the mod has set itself up, to finish off any course module settings 602 * (set instance id, add to correct section, set visibility, etc.) and send the response 603 * 604 * @param int $instanceid id returned by the mod when it was created 605 */ 606 protected function finish_setup_course_module($instanceid) { 607 global $DB, $USER; 608 609 if (!$instanceid) { 610 // Something has gone wrong - undo everything we can. 611 course_delete_module($this->cm->id); 612 throw new moodle_exception('errorcreatingactivity', 'moodle', '', $this->module->name); 613 } 614 615 // Note the section visibility 616 $visible = get_fast_modinfo($this->course)->get_section_info($this->section)->visible; 617 618 $DB->set_field('course_modules', 'instance', $instanceid, array('id' => $this->cm->id)); 619 620 \course_modinfo::purge_course_module_cache($this->course->id, $this->cm->id); 621 // Rebuild the course cache after update action 622 rebuild_course_cache($this->course->id, true, true); 623 624 $sectionid = course_add_cm_to_section($this->course, $this->cm->id, $this->section); 625 626 set_coursemodule_visible($this->cm->id, $visible); 627 if (!$visible) { 628 $DB->set_field('course_modules', 'visibleold', 1, array('id' => $this->cm->id)); 629 } 630 631 // retrieve the final info about this module. 632 $info = get_fast_modinfo($this->course); 633 if (!isset($info->cms[$this->cm->id])) { 634 // The course module has not been properly created in the course - undo everything. 635 course_delete_module($this->cm->id); 636 throw new moodle_exception('errorcreatingactivity', 'moodle', '', $this->module->name); 637 } 638 $mod = $info->get_cm($this->cm->id); 639 640 // Trigger course module created event. 641 $event = \core\event\course_module_created::create_from_cm($mod); 642 $event->trigger(); 643 644 $this->send_response($mod); 645 } 646 647 /** 648 * Send the details of the newly created activity back to the client browser 649 * 650 * @param cm_info $mod details of the mod just created 651 */ 652 protected function send_response($mod) { 653 global $OUTPUT, $PAGE; 654 655 $resp = new stdClass(); 656 $resp->error = self::ERROR_OK; 657 $resp->elementid = 'module-' . $mod->id; 658 $resp->cmid = $mod->id; 659 660 $format = course_get_format($this->course); 661 $renderer = $format->get_renderer($PAGE); 662 $modinfo = $format->get_modinfo(); 663 $section = $modinfo->get_section_info($mod->sectionnum); 664 665 // Get the new element html content. 666 $resp->fullcontent = $renderer->course_section_updated_cm_item($format, $section, $mod); 667 668 echo $OUTPUT->header(); 669 echo json_encode($resp); 670 die(); 671 } 672 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body