MDL-41580 SCORM: allow imsmanifest.xml to be used in file system repos
authorDan Marsden <dan@danmarsden.com>
Wed, 4 Sep 2013 06:46:35 +0000 (18:46 +1200)
committerDan Marsden <dan@danmarsden.com>
Tue, 24 Sep 2013 00:08:38 +0000 (12:08 +1200)
lib/filestorage/stored_file.php
mod/scorm/lang/en/scorm.php
mod/scorm/lib.php
mod/scorm/loadSCO.php
mod/scorm/locallib.php
mod/scorm/mod_form.php
repository/filesystem/lang/en/repository_filesystem.php
repository/filesystem/lib.php
repository/filesystem/tests/generator/lib.php
repository/lib.php
repository/upgrade.txt

index f1e6c24..93b7cda 100644 (file)
@@ -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();
+        }
+    }
 }
index f39efda..3f76a51 100644 (file)
@@ -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:<pre>{$a->cmsg}</pre>';
 $string['invalidhacpsession'] = 'Invalid HACP session';
index a3d17cd..7f15a2a 100644 (file)
@@ -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;
     }
index 8b39f3e..330944e 100644 (file)
@@ -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";
 }
 
index b1cde82..5afe7a7 100644 (file)
@@ -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)) {
index 99d9d87..209ef3b 100644 (file)
@@ -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) {
index ab32393..df3eeaf 100644 (file)
@@ -30,9 +30,12 @@ $string['filesystem:view'] = 'View file system repository';
 $string['information'] = 'These folders are within the <b>{$a}</b> 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 <b>{$a}</b> 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)';
+
index 42af49e..bc7ba2e 100644 (file)
@@ -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');
+    }
 }
 
 /**
index 8600686..e298c92 100644 (file)
@@ -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;
     }
 
index 276c0e9..86c5061 100644 (file)
@@ -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;
+    }
 }
 
 /**
index bd72442..2e03f7a 100644 (file)
@@ -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 ===