diff --git a/lib/filestorage/stored_file.php b/lib/filestorage/stored_file.php index f1e6c248825..93b7cda0c80 100644 --- a/lib/filestorage/stored_file.php +++ b/lib/filestorage/stored_file.php @@ -997,4 +997,19 @@ class stored_file { $this->repository->import_external_file_contents($this, $maxbytes); } } + + /** + * Gets a file relative to this file in the repository and sends it to the browser. + * Checks the function repository::supports_relative_file() to make sure it can be used. + * + * @param string $relativepath the relative path to the file we are trying to access + */ + public function send_relative_file($relativepath) { + if ($this->repository && $this->repository->supports_relative_file()) { + $relativepath = clean_param($relativepath, PARAM_PATH); + $this->repository->send_relative_file($this, $relativepath); + } else { + send_file_not_found(); + } + } } diff --git a/mod/scorm/lang/en/scorm.php b/mod/scorm/lang/en/scorm.php index f39efdaa8fe..3f76a513d12 100644 --- a/mod/scorm/lang/en/scorm.php +++ b/mod/scorm/lang/en/scorm.php @@ -33,6 +33,7 @@ $string['activityoverview'] = 'You have SCORM packages that need attention'; $string['activitypleasewait'] = 'Activity loading, please wait ...'; $string['adminsettings'] = 'Admin settings'; $string['advanced'] = 'Parameters'; +$string['aliasonly'] = 'When selecting an imsmanifest.xml file from a repository you must use an alias/shortcut for this file.'; $string['allowapidebug'] = 'Activate API debug and tracing (set the capture mask with apidebugmask)'; $string['allowtypeexternal'] = 'Enable external package type'; $string['allowtypeexternalaicc'] = 'Enable direct AICC URL'; @@ -167,6 +168,7 @@ $string['identifier'] = 'Question identifier'; $string['incomplete'] = 'Incomplete'; $string['info'] = 'Info'; $string['interactions'] = 'Interactions'; +$string['repositorynotsupported'] = 'Only file system repositories are supported when linking directly to an imsmanifest.xml file.'; $string['trackid'] = 'Id'; $string['trackid_help'] = 'This is the identifier set by your SCORM package for this question, the SCORM specification doesn\'t allow the full question text to be provided.'; $string['trackcorrectcount'] = 'Correct count'; @@ -194,6 +196,7 @@ $string['tracktype_help'] = 'Type of the question, for example "choice" or "shor $string['trackweight'] = 'Weight'; $string['trackweight_help'] = 'Weight assigned to the question when calculating score.'; $string['invalidactivity'] = 'SCORM activity is incorrect'; +$string['invalidmanifestname'] = 'Only imsmanifest.xml or .zip files may be selected'; $string['invalidurl'] = 'Invalid URL specified'; $string['invalidurlhttpcheck'] = 'Invalid URL specified. Debug message:
{$a->cmsg}
'; $string['invalidhacpsession'] = 'Invalid HACP session'; diff --git a/mod/scorm/lib.php b/mod/scorm/lib.php index a3d17cd7508..7f15a2abe70 100644 --- a/mod/scorm/lib.php +++ b/mod/scorm/lib.php @@ -953,6 +953,22 @@ function scorm_pluginfile($course, $cm, $context, $filearea, $args, $forcedownlo $fullpath = "/$context->id/mod_scorm/package/0/$relativepath"; $lifetime = 0; // no caching here + } else if ($filearea === 'imsmanifest') { // This isn't a real filearea, it's a url parameter for this type of package. + $revision = (int)array_shift($args); // Prevents caching problems - ignored here. + $relativepath = implode('/', $args); + + // Get imsmanifest file. + $fs = get_file_storage(); + $files = $fs->get_area_files($context->id, 'mod_scorm', 'package', 0, '', false); + $file = reset($files); + + // Check that the package file is an imsmanifest.xml file - if not then this method is not allowed. + $packagefilename = $file->get_filename(); + if (strtolower($packagefilename) !== 'imsmanifest.xml') { + return false; + } + + $file->send_relative_file($relativepath); } else { return false; } diff --git a/mod/scorm/loadSCO.php b/mod/scorm/loadSCO.php index 8b39f3e0fdf..330944e2dc2 100644 --- a/mod/scorm/loadSCO.php +++ b/mod/scorm/loadSCO.php @@ -148,11 +148,14 @@ if (scorm_external_link($sco->launch)) { //TODO: does this happen? $result = $launcher; } else if ($scorm->scormtype === SCORM_TYPE_EXTERNAL) { - // Remote learning activity + // Remote learning activity. $result = dirname($scorm->reference).'/'.$launcher; +} else if ($scorm->scormtype === SCORM_TYPE_LOCAL && strtolower($scorm->reference) == 'imsmanifest.xml') { + // This SCORM content sits in a repository that allows relative links. + $result = "$CFG->wwwroot/pluginfile.php/$context->id/mod_scorm/imsmanifest/$scorm->revision/$launcher"; } else if ($scorm->scormtype === SCORM_TYPE_LOCAL or $scorm->scormtype === SCORM_TYPE_LOCALSYNC) { - //note: do not convert this to use get_file_url() or moodle_url() - //SCORM does not work without slasharguments and moodle_url() encodes querystring vars + // Note: do not convert this to use get_file_url() or moodle_url() + // SCORM does not work without slasharguments and moodle_url() encodes querystring vars. $result = "$CFG->wwwroot/pluginfile.php/$context->id/mod_scorm/content/$scorm->revision/$launcher"; } diff --git a/mod/scorm/locallib.php b/mod/scorm/locallib.php index b1cde822434..5afe7a7f50b 100644 --- a/mod/scorm/locallib.php +++ b/mod/scorm/locallib.php @@ -199,6 +199,7 @@ function scorm_parse($scorm, $full) { $fs = get_file_storage(); $packagefile = false; + $packagefileimsmanifest = false; if ($scorm->scormtype === SCORM_TYPE_LOCAL) { if ($packagefile = $fs->get_file($context->id, 'mod_scorm', 'package', 0, '/', $scorm->reference)) { @@ -206,6 +207,9 @@ function scorm_parse($scorm, $full) { $packagefile->import_external_file_contents(); } $newhash = $packagefile->get_contenthash(); + if (strtolower($packagefile->get_filename()) == 'imsmanifest.xml') { + $packagefileimsmanifest = true; + } } else { $newhash = null; } @@ -228,8 +232,8 @@ function scorm_parse($scorm, $full) { if ($packagefile) { if (!$full and $packagefile and $scorm->sha1hash === $newhash) { if (strpos($scorm->version, 'SCORM') !== false) { - if ($fs->get_file($context->id, 'mod_scorm', 'content', 0, '/', 'imsmanifest.xml')) { - // no need to update + if ($packagefileimsmanifest || $fs->get_file($context->id, 'mod_scorm', 'content', 0, '/', 'imsmanifest.xml')) { + // No need to update. return; } } else if (strpos($scorm->version, 'AICC') !== false) { @@ -237,18 +241,25 @@ function scorm_parse($scorm, $full) { return; } } + if (!$packagefileimsmanifest) { + // Now extract files. + $fs->delete_area_files($context->id, 'mod_scorm', 'content'); - // now extract files - $fs->delete_area_files($context->id, 'mod_scorm', 'content'); - - $packer = get_file_packer('application/zip'); - $packagefile->extract_to_storage($packer, $context->id, 'mod_scorm', 'content', 0, '/'); + $packer = get_file_packer('application/zip'); + $packagefile->extract_to_storage($packer, $context->id, 'mod_scorm', 'content', 0, '/'); + } } else if (!$full) { return; } + if ($packagefileimsmanifest) { + require_once("$CFG->dirroot/mod/scorm/datamodels/scormlib.php"); + // Direct link to imsmanifest.xml file. + if (!scorm_parse_scorm($scorm, $packagefile)) { + $scorm->version = 'ERROR'; + } - if ($manifest = $fs->get_file($context->id, 'mod_scorm', 'content', 0, '/', 'imsmanifest.xml')) { + } else if ($manifest = $fs->get_file($context->id, 'mod_scorm', 'content', 0, '/', 'imsmanifest.xml')) { require_once("$CFG->dirroot/mod/scorm/datamodels/scormlib.php"); // SCORM if (!scorm_parse_scorm($scorm, $manifest)) { diff --git a/mod/scorm/mod_form.php b/mod/scorm/mod_form.php index 99d9d875f90..209ef3b0b91 100644 --- a/mod/scorm/mod_form.php +++ b/mod/scorm/mod_form.php @@ -89,7 +89,7 @@ class mod_scorm_mod_form extends moodleform_mod { // New local package upload. $filemanageroptions = array(); - $filemanageroptions['accepted_types'] = array('.zip'); + $filemanageroptions['accepted_types'] = array('.zip', '.xml'); $filemanageroptions['maxbytes'] = 0; $filemanageroptions['maxfiles'] = 1; $filemanageroptions['subdirs'] = 0; @@ -353,7 +353,21 @@ class mod_scorm_mod_form extends moodleform_mod { // Make sure updatefreq is not set if using normal local file. $errors['updatefreq'] = get_string('updatefreq_error', 'mod_scorm'); } - $errors = array_merge($errors, scorm_validate_package($file)); + if (strtolower($file->get_filename()) == 'imsmanifest.xml') { + if (!$file->is_external_file()) { + $errors['packagefile'] = get_string('aliasonly', 'mod_scorm'); + } else { + $repository = repository::get_repository_by_id($file->get_repository_id(), CONTEXT_SYSTEM); + if (!$repository->supports_relative_file()) { + $errors['packagefile'] = get_string('repositorynotsupported', 'mod_scorm'); + } + } + } else if (strtolower(substr($file->get_filename(), -3)) == 'xml') { + $errors['packagefile'] = get_string('invalidmanifestname', 'mod_scorm'); + } else { + // Validate this SCORM package. + $errors = array_merge($errors, scorm_validate_package($file)); + } } } else if ($type === SCORM_TYPE_EXTERNAL) { diff --git a/repository/filesystem/lang/en/repository_filesystem.php b/repository/filesystem/lang/en/repository_filesystem.php index ab323933b92..df3eeaf4ed4 100644 --- a/repository/filesystem/lang/en/repository_filesystem.php +++ b/repository/filesystem/lang/en/repository_filesystem.php @@ -30,9 +30,12 @@ $string['filesystem:view'] = 'View file system repository'; $string['information'] = 'These folders are within the {$a} directory.'; $string['invalidpath'] = 'Invalid root path'; $string['path'] = 'Select a subdirectory'; +$string['relativefiles'] = 'Allow relative files'; +$string['relativefiles_desc'] = 'This allows all files in the repository to be accessible using relative links.'; $string['root'] = 'Root'; $string['nosubdir'] = 'You need to create at least one folder inside the {$a} directory so you can select it here.'; $string['pluginname_help'] = 'Create repository from local directory'; $string['pluginname'] = 'File system'; $string['enablecourseinstances'] = 'Allow admins to add a file system repository instance to a course (configurable only by admins)'; $string['enableuserinstances'] = 'Allow admins to add a file system repository instance for personal use (configurable only by admins)'; + diff --git a/repository/filesystem/lib.php b/repository/filesystem/lib.php index 42af49ec9d8..bc7ba2ea4b7 100644 --- a/repository/filesystem/lib.php +++ b/repository/filesystem/lib.php @@ -194,11 +194,12 @@ class repository_filesystem extends repository { } public static function get_instance_option_names() { - return array('fs_path'); + return array('fs_path', 'relativefiles'); } public function set_option($options = array()) { $options['fs_path'] = clean_param($options['fs_path'], PARAM_PATH); + $options['relativefiles'] = clean_param($options['relativefiles'], PARAM_INT); $ret = parent::set_option($options); return $ret; } @@ -229,6 +230,10 @@ class repository_filesystem extends repository { } closedir($handle); } + $mform->addElement('checkbox', 'relativefiles', get_string('relativefiles', 'repository_filesystem'), + get_string('relativefiles_desc', 'repository_filesystem')); + $mform->setType('relativefiles', PARAM_INT); + } else { $mform->addElement('static', null, '', get_string('nopermissions', 'error', get_string('configplugin', 'repository_filesystem'))); return false; @@ -461,6 +466,44 @@ class repository_filesystem extends repository { mtrace(" instance {$this->id}: deleted $deletedcount thumbnails"); } } + + /** + * Gets a file relative to this file in the repository and sends it to the browser. + * + * @param stored_file $mainfile The main file we are trying to access relative files for. + * @param string $relativepath the relative path to the file we are trying to access. + */ + public function send_relative_file(stored_file $mainfile, $relativepath) { + global $CFG; + // Check if this repository is allowed to use relative linking. + $allowlinks = $this->supports_relative_file(); + $lifetime = isset($CFG->filelifetime) ? $CFG->filelifetime : 86400; + if (!empty($allowlinks)) { + // Get path to the mainfile. + $mainfilepath = $mainfile->get_source(); + + // Strip out filename from the path. + $filename = $mainfile->get_filename(); + $basepath = strstr($mainfilepath, $filename, true); + + $fullrelativefilepath = realpath($this->root_path.$basepath.$relativepath); + + // Sanity check to make sure this path is inside this repository and the file exists. + if (strpos($fullrelativefilepath, $this->root_path) === 0 && file_exists($fullrelativefilepath)) { + send_file($fullrelativefilepath, basename($relativepath), $lifetime, 0); + } + } + send_file_not_found(); + } + + /** + * helper function to check if the repository supports send_relative_file. + * + * @return true|false + */ + public function supports_relative_file() { + return $this->get_option('relativefiles'); + } } /** diff --git a/repository/filesystem/tests/generator/lib.php b/repository/filesystem/tests/generator/lib.php index 8600686ac02..e298c92abb9 100644 --- a/repository/filesystem/tests/generator/lib.php +++ b/repository/filesystem/tests/generator/lib.php @@ -44,6 +44,9 @@ class repository_filesystem_generator extends testing_repository_generator { if (!isset($record['fs_path'])) { $record['fs_path'] = '/i/do/not/exist'; } + if (!isset($record['relativefiles'])) { + $record['relativefiles'] = 0; + } return $record; } diff --git a/repository/lib.php b/repository/lib.php index 276c0e9edb0..86c50618920 100644 --- a/repository/lib.php +++ b/repository/lib.php @@ -2866,6 +2866,31 @@ abstract class repository implements cacheable_object { $classname = $data['class']; return new $classname($data['id'], $data['ctxid'], $data['options'], $data['readonly']); } + + /** + * Gets a file relative to this file in the repository and sends it to the browser. + * Used to allow relative file linking within a repository without creating file records + * for linked files + * + * Repositories that overwrite this must be very careful - see filesystem repository for example. + * + * @param stored_file $mainfile The main file we are trying to access relative files for. + * @param string $relativepath the relative path to the file we are trying to access. + * + */ + public function send_relative_file(stored_file $mainfile, $relativepath) { + // This repository hasn't implemented this so send_file_not_found. + send_file_not_found(); + } + + /** + * helper function to check if the repository supports send_relative_file. + * + * @return true|false + */ + public function supports_relative_file() { + return false; + } } /** diff --git a/repository/upgrade.txt b/repository/upgrade.txt index bd724428512..2e03f7a6987 100644 --- a/repository/upgrade.txt +++ b/repository/upgrade.txt @@ -8,6 +8,9 @@ http://docs.moodle.org/dev/Repository_API * get_option() now always return null when the first parameter ($config) is not empty, and no value was found for this $config. Previously this could sometimes return an empty array(). * The function repository_attach_id() was removed, it was never used and was not useful. +* New functions send_relative_file() and supports_relative_file() to allow sending relative linked + files - see filesystem repository for example. + === 2.5 ===