diff --git a/components/ILIAS/Scorm2004/classes/class.ilSCORM13PlayerGUI.php b/components/ILIAS/Scorm2004/classes/class.ilSCORM13PlayerGUI.php index f1d97884478c..8e212ca3c337 100755 --- a/components/ILIAS/Scorm2004/classes/class.ilSCORM13PlayerGUI.php +++ b/components/ILIAS/Scorm2004/classes/class.ilSCORM13PlayerGUI.php @@ -364,7 +364,9 @@ public function getPlayer(): void global $DIC; $lng = $DIC->language(); $ilSetting = $DIC->settings(); - ilWACSignedPath::signFolderOfStartFile($this->getDataDirectory() . '/imsmanifest.xml'); + if (!$this->slm->hasContainerResource()) { + ilWACSignedPath::signFolderOfStartFile($this->getDataDirectory() . '/imsmanifest.xml'); + } // player basic config data @@ -419,7 +421,7 @@ public function getPlayer(): void $config['scorm_player_unload_url'] = $unload_url; $config['post_log_url'] = $DIC->ctrl()->getLinkTarget($this, 'postLogEntry'); $config['livelog_url'] = $DIC->ctrl()->getLinkTarget($this, 'liveLogContent'); - $config['package_url'] = $this->getDataDirectory() . "/"; + $config['package_url'] = $this->slm->getContainerBaseUri() ?? ($this->getDataDirectory() . "/"); //editor $config['envEditor'] = 0; @@ -454,12 +456,15 @@ public function getPlayer(): void $jQueryPath = str_replace('.min', '', iljQueryUtil::getLocaljQueryPath()); $this->tpl->setVariable("JS_FILE", $jQueryPath); - // include ilias rte css, if given - $rte_css = $this->slm->getDataDirectory() . "/ilias_css_4_2/css/style.css"; - if (is_file($rte_css)) { - $this->tpl->setCurrentBlock("rte_css"); - $this->tpl->setVariable("RTE_CSS", $rte_css); - $this->tpl->parseCurrentBlock(); + // include ilias rte css, if given (only legacy on-disk modules carry the + // ilias_css_4_2 editor stylesheet; imported/migrated packages do not) + if (!$this->slm->hasContainerResource()) { + $rte_css = $this->slm->getDataDirectory() . "/ilias_css_4_2/css/style.css"; + if (is_file($rte_css)) { + $this->tpl->setCurrentBlock("rte_css"); + $this->tpl->setVariable("RTE_CSS", $rte_css); + $this->tpl->parseCurrentBlock(); + } } @@ -607,7 +612,9 @@ public function getADLActData(): void public function pingSession(): void { - ilWACSignedPath::signFolderOfStartFile($this->getDataDirectory() . '/imsmanifest.xml'); + if (!$this->slm->hasContainerResource()) { + ilWACSignedPath::signFolderOfStartFile($this->getDataDirectory() . '/imsmanifest.xml'); + } //do nothing except returning header header('Content-Type: text/plain; charset=UTF-8'); print(""); diff --git a/components/ILIAS/ScormAicc/ScormAicc.php b/components/ILIAS/ScormAicc/ScormAicc.php index 1b0dbf9e7cca..38ec936f76e2 100644 --- a/components/ILIAS/ScormAicc/ScormAicc.php +++ b/components/ILIAS/ScormAicc/ScormAicc.php @@ -32,6 +32,11 @@ public function init( array | \ArrayAccess &$pull, array | \ArrayAccess &$internal, ): void { + $contribute[\ILIAS\Setup\Agent::class] = static fn() => + new \ilScormAiccSetupAgent( + $pull[\ILIAS\Refinery\Factory::class] + ); + $contribute[Component\Resource\PublicAsset::class] = static fn() => new class () implements Component\Resource\PublicAsset { public function getSource(): string { diff --git a/components/ILIAS/ScormAicc/classes/SCORM/class.ilObjSCORMInitData.php b/components/ILIAS/ScormAicc/classes/SCORM/class.ilObjSCORMInitData.php index 56e19b5e3a4b..94b4982f78a9 100755 --- a/components/ILIAS/ScormAicc/classes/SCORM/class.ilObjSCORMInitData.php +++ b/components/ILIAS/ScormAicc/classes/SCORM/class.ilObjSCORMInitData.php @@ -188,7 +188,7 @@ public static function getIliasScormVars(ilObjSCORMLearningModule $slm_obj): str . '"b_debug":' . $b_debug . ',' . '"a_itemParameter":' . json_encode($a_man) . ',' . '"status":' . json_encode(self::getStatus($slm_obj->getId(), $ilUser->getID(), $slm_obj->getAuto_last_visited())) . ',' - . '"dataDirectory":"' . self::encodeURIComponent($slm_obj->getDataDirectory("output") . '/') . '",' + . '"dataDirectory":"' . self::encodeURIComponent($slm_obj->getContainerBaseUri() ?? ($slm_obj->getDataDirectory("output") . '/')) . '",' . '"img":{' . '"asset":"' . self::encodeURIComponent(ilUtil::getImagePath('scorm/asset.svg')) . '",' . '"browsed":"' . self::encodeURIComponent(ilUtil::getImagePath('scorm/browsed.svg')) . '",' diff --git a/components/ILIAS/ScormAicc/classes/SCORM/class.ilSCORMPresentationGUI.php b/components/ILIAS/ScormAicc/classes/SCORM/class.ilSCORMPresentationGUI.php index 70a8ec1cd33f..7beb4cf70ddc 100755 --- a/components/ILIAS/ScormAicc/classes/SCORM/class.ilSCORMPresentationGUI.php +++ b/components/ILIAS/ScormAicc/classes/SCORM/class.ilSCORMPresentationGUI.php @@ -117,7 +117,9 @@ public function frameset(): void // } $this->increase_attemptAndsave_module_version(); - ilWACSignedPath::signFolderOfStartFile($this->slm->getDataDirectory() . '/imsmanifest.xml'); + if (!$this->slm->hasContainerResource()) { + ilWACSignedPath::signFolderOfStartFile($this->slm->getDataDirectory() . '/imsmanifest.xml'); + } $debug = $this->slm->getDebug(); if (count($items) > 1) { @@ -487,7 +489,9 @@ public function apiInitData(): void public function pingSession(): bool { - ilWACSignedPath::signFolderOfStartFile($this->slm->getDataDirectory() . '/imsmanifest.xml'); + if (!$this->slm->hasContainerResource()) { + ilWACSignedPath::signFolderOfStartFile($this->slm->getDataDirectory() . '/imsmanifest.xml'); + } return true; } diff --git a/components/ILIAS/ScormAicc/classes/Setup/Migrations/class.ilSAHSMigration.php b/components/ILIAS/ScormAicc/classes/Setup/Migrations/class.ilSAHSMigration.php new file mode 100644 index 000000000000..17195fae3432 --- /dev/null +++ b/components/ILIAS/ScormAicc/classes/Setup/Migrations/class.ilSAHSMigration.php @@ -0,0 +1,132 @@ +) into a container resource of the + * Resource Storage Service and stores the resulting rid in sahs_lm. + * + * Covers all c_type values (scorm, scorm2004, aicc) since they share + * the same lm_data/lm_ directory layout. + */ +class ilSAHSMigration implements Migration +{ + protected ilResourceStorageMigrationHelper $helper; + + public function getLabel(): string + { + return 'Migration of SCORM/AICC Learning Modules to the Resource Storage Service.'; + } + + public function getDefaultAmountOfStepsPerRun(): int + { + return 10000; + } + + public function getPreconditions(Environment $environment): array + { + return array_merge( + ilResourceStorageMigrationHelper::getPreconditions(), + [ + new ilDatabaseUpdateStepsExecutedObjective( + new \ILIAS\ScormAicc\Setup\ScormAiccDatabaseUpdateSteps() + ) + ] + ); + } + + public function prepare(Environment $environment): void + { + $this->helper = new ilResourceStorageMigrationHelper( + new ilSAHSStakeholder(), + $environment + ); + } + + public function step(Environment $environment): void + { + $r = $this->helper->getDatabase()->query( + "SELECT sahs_lm.id, object_data.owner AS owner_id + FROM sahs_lm + LEFT JOIN object_data ON object_data.obj_id = sahs_lm.id + WHERE rid IS NULL OR rid = '' + LIMIT 1;" + ); + + $d = $this->helper->getDatabase()->fetchObject($r); + $object_id = (int) ($d->id ?? null); + + $resource_owner_id = (int) ($d->owner_id ?? 6); + + $lm_path = $this->buildBasePath($object_id); + + $rid = $this->helper->moveDirectoryToContainerResource( + $lm_path, + $resource_owner_id + ); + + if ($rid !== null) { + $this->helper->getDatabase()->update( + 'sahs_lm', + ['rid' => ['text', $rid->serialize()]], + ['id' => ['integer', $object_id],] + ); + + $this->recursiveRmDir($lm_path); + } else { + $this->helper->getDatabase()->update( + 'sahs_lm', + ['rid' => ['text', '-']], + ['id' => ['integer', $object_id],] + ); + } + } + + private function recursiveRmDir(string $path): void + { + if (!is_dir($path)) { + return; + } + // recursively remove directory + $files = array_diff(scandir($path), ['.', '..']); + foreach ($files as $file) { + (is_dir("$path/$file")) ? $this->recursiveRmDir("$path/$file") : unlink("$path/$file"); + } + rmdir($path); + } + + public function getRemainingAmountOfSteps(): int + { + $r = $this->helper->getDatabase()->query( + "SELECT COUNT(id) AS amount FROM sahs_lm WHERE rid IS NULL OR rid = ''" + ); + $d = $this->helper->getDatabase()->fetchObject($r) ?? new stdClass(); + + return (int) ($d->amount ?? 0); + } + + protected function buildBasePath(int $object_id): string + { + return CLIENT_WEB_DIR . '/lm_data/lm_' . $object_id; + } +} diff --git a/components/ILIAS/ScormAicc/classes/Setup/class.ilScormAiccSetupAgent.php b/components/ILIAS/ScormAicc/classes/Setup/class.ilScormAiccSetupAgent.php new file mode 100644 index 000000000000..a268a670a7ac --- /dev/null +++ b/components/ILIAS/ScormAicc/classes/Setup/class.ilScormAiccSetupAgent.php @@ -0,0 +1,42 @@ +setIdSetting((int) $lm_rec["id_setting"]); $this->setNameSetting((int) $lm_rec["name_setting"]); + $this->setRID((string) ($lm_rec["rid"] ?? '')); if (ilObject::_lookupType($this->getStyleSheetId()) !== "sty") { $this->setStyleSheetId(0); } @@ -303,6 +308,218 @@ public function getDataDirectory(?string $mode = "filesystem"): string return $lm_data_dir . "/lm_" . $this->getId(); } + public function getRID(): ?string + { + return $this->rid; + } + + public function setRID(?string $rid): void + { + $this->rid = $rid; + } + + protected function getIrss(): \ILIAS\ResourceStorage\Services + { + global $DIC; + return $DIC->resourceStorage(); + } + + /** + * True when the module's content lives in a Resource Storage container. + * The rid sentinel '-' marks a module whose migration failed, '' an + * un-migrated module; both fall back to the on-disk data directory. + */ + public function hasContainerResource(): bool + { + return $this->getResource() !== null; + } + + public function getResource(): ?StorableContainerResource + { + $rid = $this->getRID(); + if ($rid === null || $rid === '' || $rid === '-') { + return null; + } + $identification = $this->getIrss()->manage()->find($rid); + if ($identification === null) { + return null; + } + $resource = $this->getIrss()->manage()->getResource($identification); + return $resource instanceof StorableContainerResource ? $resource : null; + } + + /** + * Base URL of the content container, ending in the sub-request separator + * so the player can append relative paths (e.g. "/-/sco1/index.html"). + * Returns null for un-migrated modules; callers must then fall back to the + * web path of getDataDirectory(). + */ + public function getContainerBaseUri(float $valid_for_at_least_minutes = 480.0): ?string + { + $resource = $this->getResource(); + if ($resource === null) { + return null; + } + $uri = $this->getIrss()->consume()->containerURI( + $resource->getIdentification(), + '', + $valid_for_at_least_minutes + )->getURI(); + if ($uri === null) { + return null; + } + + // The player appends relative file paths to this base, so it must end + // with the sub-request separator "/-/". new URI() strips the trailing + // slash for an empty start file, leaving ".../-" — restore it, otherwise + // requests miss the separator and the whole container ZIP is delivered. + $base = (string) $uri; + if (str_ends_with($base, '/-')) { + $base .= '/'; + } + return $base; + } + + public function removeContainerResource(): void + { + $resource = $this->getResource(); + if ($resource === null) { + return; + } + $this->getIrss()->manage()->remove( + $resource->getIdentification(), + new ilSAHSStakeholder() + ); + $this->setRID(''); + } + + /** + * Read the imsmanifest.xml of the current container revision (migrated + * modules). Returns null when the module is not migrated or the manifest + * is absent. Used to validate that a new package version matches the + * existing manifest structure (cp_* tables) before replacing the resource. + */ + public function getManifestFromContainer(string $manifest_file = 'imsmanifest.xml'): ?string + { + $resource = $this->getResource(); + if ($resource === null) { + return null; + } + $zip = $this->getIrss()->consume()->containerZIP($resource->getIdentification())->getZIP(); + $files = iterator_to_array($zip->getFiles(), false); + foreach ($zip->getFileStreams() as $i => $stream) { + if (isset($files[$i]) && basename((string) $files[$i]) === $manifest_file) { + return (string) $stream; + } + } + return null; + } + + /** + * Replace the content of the module's container resource with a new ZIP + * as a new revision (migrated modules only). Returns false when the module + * is not migrated. + */ + public function replaceContainerFromZipPath(string $absolute_zip_path, ?string $revision_title = null): bool + { + $resource = $this->getResource(); + if ($resource === null) { + return false; + } + $handle = fopen($absolute_zip_path, 'rb'); + if ($handle === false) { + return false; + } + $this->getIrss()->manage()->appendNewRevisionFromStream( + $resource->getIdentification(), + \ILIAS\Filesystem\Stream\Streams::ofResource($handle), + new ilSAHSStakeholder(), + $revision_title ?? basename($absolute_zip_path) + ); + return true; + } + + /** + * Move the on-disk data directory into a new container resource and remove + * the directory. Call AFTER readObject() — manifest parsing reads the files + * from disk. The zip keeps the directory structure but strips the top + * lm_ folder, so files end up at the container root. + */ + public function moveDataDirectoryToContainer(): bool + { + $dir = $this->getDataDirectory(); + if (!is_dir($dir)) { + return false; + } + $zip = new \ILIAS\Filesystem\Util\Archive\Zip( + (new \ILIAS\Filesystem\Util\Archive\ZipOptions()) + ->withDirectoryHandling(\ILIAS\Filesystem\Util\Archive\ZipDirectoryHandling::KEEP_STRUCTURE) + ); + $zip->addDirectory($dir); + try { + $zip_stream = $zip->get(); + } catch (\Throwable) { + $zip->destroy(); + return false; + } + $rid = $this->getIrss()->manageContainer()->containerFromStream( + $zip_stream, + new ilSAHSStakeholder(), + $this->getTitle() + ); + $zip->destroy(); + + $this->setRID($rid->serialize()); + $this->update(); + + ilFileUtils::delDir($dir); + return true; + } + + /** + * Extract the current container revision back onto disk at + * getDataDirectory(). Used to re-parse the manifest (cp_* tables) for a + * freshly cloned module. The caller must remove the directory afterwards. + */ + public function extractContainerToDataDirectory(): bool + { + $resource = $this->getResource(); + if ($resource === null) { + return false; + } + $this->createDataDirectory(); + $options = (new \ILIAS\Filesystem\Util\Archive\UnzipOptions()) + ->withZipOutputPath($this->getDataDirectory()) + ->withOverwrite(true); + + return $this->getIrss() + ->consume() + ->containerZIP($resource->getIdentification()) + ->getZIP($options) + ->extract(); + } + + /** + * Write the module content to $target_zip_path as a zip without the + * lm_ top directory (export "content.zip"). Sources from the container + * for migrated modules, otherwise from the on-disk data directory. + */ + public function writeContentZip(string $target_zip_path): void + { + global $DIC; + $cleanup = false; + if ($this->hasContainerResource()) { + $this->extractContainerToDataDirectory(); + $cleanup = true; + } + + $DIC->legacyArchives()->zip($this->getDataDirectory(), $target_zip_path, false); + + if ($cleanup) { + ilFileUtils::delDir($this->getDataDirectory()); + } + } + /** * get api adapter name */ @@ -858,7 +1075,8 @@ public function update(): bool ie_force_render = %s, mastery_score = %s, id_setting = %s, - name_setting = %s + name_setting = %s, + rid = %s WHERE id = %s', array( 'text', 'text', @@ -895,6 +1113,7 @@ public function update(): bool 'integer', 'integer', 'integer', + 'text', 'integer' ), array( $this->getAPIAdapterName(), @@ -932,6 +1151,7 @@ public function update(): bool $this->getMasteryScore(), $this->getIdSetting(), $this->getNameSetting(), + $this->getRID() ?? '', $this->getId()) ); @@ -1009,6 +1229,9 @@ public function delete(): bool // delete meta data of scorm content object $this->deleteMetaData(); + // delete content container resource (migrated modules) + $this->removeContainerResource(); + // delete data directory ilFileUtils::delDir($this->getDataDirectory()); @@ -1257,16 +1480,24 @@ public function cloneObject(int $a_target_id, int $a_copy_id = 0, bool $a_omit_t break; } - // copy data directory - $new_obj->populateByDirectoy($source_obj->getDataDirectory()); - - // // copy authored content ... - // if ($new_obj->getEditable()) { - // $source_obj->copyAuthoredContent($new_obj); - // } else { - // ... or read manifest file - $new_obj->readObject(); - // } + if ($source_obj->hasContainerResource()) { + // copy the content container into a new resource for the clone + $new_rid = $this->getIrss()->manage()->clone( + $source_obj->getResource()->getIdentification() + ); + $new_obj->setRID($new_rid->serialize()); + $new_obj->update(); + + // re-parse the manifest (cp_* tables) for the new object id; + // readObject() reads from disk, so extract the container briefly + $new_obj->extractContainerToDataDirectory(); + $new_obj->readObject(); + ilFileUtils::delDir($new_obj->getDataDirectory()); + } else { + // legacy on-disk module: copy the directory and re-parse + $new_obj->populateByDirectoy($source_obj->getDataDirectory()); + $new_obj->readObject(); + } $obj_settings = new ilLPObjSettings($this->getId()); $obj_settings->cloneSettings($new_obj->getId()); /** @var ilScormLP $olp */ diff --git a/components/ILIAS/ScormAicc/classes/class.ilObjSAHSLearningModuleGUI.php b/components/ILIAS/ScormAicc/classes/class.ilObjSAHSLearningModuleGUI.php index 58df4b031ab5..8f653cc5ef9b 100755 --- a/components/ILIAS/ScormAicc/classes/class.ilObjSAHSLearningModuleGUI.php +++ b/components/ILIAS/ScormAicc/classes/class.ilObjSAHSLearningModuleGUI.php @@ -27,7 +27,7 @@ * $Id$ * * @ilCtrl_Calls ilObjSAHSLearningModuleGUI: ilFileSystemGUI, ilObjectMetaDataGUI, ilPermissionGUI, ilInfoScreenGUI, ilLearningProgressGUI -* @ilCtrl_Calls ilObjSAHSLearningModuleGUI: ilCommonActionDispatcherGUI, ilExportGUI, ilObjectCopyGUI +* @ilCtrl_Calls ilObjSAHSLearningModuleGUI: ilCommonActionDispatcherGUI, ilExportGUI, ilObjectCopyGUI, ilContainerResourceGUI * * @ingroup components\ILIASScormAicc */ @@ -50,6 +50,24 @@ public function __construct($data, int $id, bool $call_by_reference, bool $prepa parent::__construct($data, $id, $call_by_reference, false); } + /** + * List files tab: forward to the read-only container view once migrated, + * otherwise show an info box. On-disk file editing is no longer offered. + */ + public function listFiles(): void + { + global $DIC; + if ($this->object->hasContainerResource()) { + $this->ctrl->redirectByClass(ilContainerResourceGUI::class); + return; + } + $this->tabs_gui->setTabActive('cont_list_files'); + $message_box = $DIC->ui()->factory()->messageBox()->info( + $this->lng->txt('infobox_files_not_migrated') + ); + $this->tpl->setContent($DIC->ui()->renderer()->render([$message_box])); + } + /** * execute command * @throws ilCtrlException @@ -117,10 +135,27 @@ public function executeCommand(): void break; case "ilfilesystemgui": - $fs_gui = new ilFileSystemGUI($this->object->getDataDirectory()); - $fs_gui->setUseUploadDirectory(true); - $fs_gui->setTableId("sahsfs" . $this->object->getId()); - $this->ctrl->forwardCommand($fs_gui); + // legacy on-disk file editing is no longer offered; content is + // managed through the Resource Storage container once migrated + $ilErr->raiseError($this->lng->txt('permission_denied'), $ilErr->WARNING); + break; + + case strtolower(ilContainerResourceGUI::class): + if (!$this->object->hasContainerResource()) { + $ilErr->raiseError($this->lng->txt('permission_denied'), $ilErr->WARNING); + } + $ilTabs->setTabActive('cont_list_files'); + $view_config = new \ILIAS\components\ResourceStorage\Container\View\Configuration( + $this->object->getResource(), + new ilSAHSStakeholder(), + $this->lng->txt('cont_list_files'), + \ILIAS\components\ResourceStorage\Container\View\Mode::DATA_TABLE, + 250, + false, // read-only: no upload + false // read-only: no administrate + ); + $container_gui = new ilContainerResourceGUI($view_config); + $this->ctrl->forwardCommand($container_gui); break; case "ilcertificategui": @@ -439,6 +474,9 @@ public function uploadObject(): void ilObject::_writeTitle($newObj->getId(), $title); } + // move the freshly uploaded package into a Resource Storage container + $newObj->moveDataDirectoryToContainer(); + //auto set learning progress settings $newObj->setLearningProgressSettingsAtUpload(); @@ -616,17 +654,15 @@ protected function getTabs(): void break; } - // file system gui tabs - // properties + // list files tab — routes to the container view (migrated) or an + // info box (not yet migrated); legacy on-disk editing is gone if ($rbacsystem->checkAccess("write", "", $this->object->getRefId())) { - $ilCtrl->setParameterByClass("ilfilesystemgui", "resetoffset", 1); $this->tabs_gui->addTarget( "cont_list_files", - $this->ctrl->getLinkTargetByClass("ilfilesystemgui", "listFiles"), + $this->ctrl->getLinkTarget($this, "listFiles"), "", - "ilfilesystemgui" + "" ); - $ilCtrl->setParameterByClass("ilfilesystemgui", "resetoffset", ""); } // info screen $force_active = ($this->ctrl->getNextClass() === "ilinfoscreengui") diff --git a/components/ILIAS/ScormAicc/classes/class.ilObjSCORMLearningModuleGUI.php b/components/ILIAS/ScormAicc/classes/class.ilObjSCORMLearningModuleGUI.php index 22ecde08b674..7e6444182354 100755 --- a/components/ILIAS/ScormAicc/classes/class.ilObjSCORMLearningModuleGUI.php +++ b/components/ILIAS/ScormAicc/classes/class.ilObjSCORMLearningModuleGUI.php @@ -24,7 +24,7 @@ * @author Alex Killing , Hendrik Holtmann * $Id$ * -* @ilCtrl_Calls ilObjSCORMLearningModuleGUI: ilFileSystemGUI, ilObjectMetaDataGUI, ilPermissionGUI, ilLearningProgressGUI +* @ilCtrl_Calls ilObjSCORMLearningModuleGUI: ilFileSystemGUI, ilObjectMetaDataGUI, ilPermissionGUI, ilLearningProgressGUI, ilContainerResourceGUI * @ilCtrl_Calls ilObjSCORMLearningModuleGUI: ilInfoScreenGUI * @ilCtrl_Calls ilObjSCORMLearningModuleGUI: ilCertificateGUI * @ilCtrl_Calls ilObjSCORMLearningModuleGUI: ilSCORMTrackingItemsPerScoFilterGUI, ilSCORMTrackingItemsPerUserFilterGUI, ilSCORMTrackingItemsTableGUI @@ -375,6 +375,17 @@ public function newModuleVersion(): void $this->setSettingsSubTabs(); $ilTabs->setSubTabActive('cont_sc_new_version'); + // a new module version replaces the content of the IRSS container, so + // the module must have been migrated to the Resource Storage first + if (!$this->object->hasContainerResource()) { + $this->tpl->setContent( + $DIC->ui()->renderer()->render([ + $DIC->ui()->factory()->messageBox()->info($this->lng->txt('infobox_files_not_migrated')) + ]) + ); + return; + } + $obj_id = ilObject::_lookupObjectId($this->refId); $type = ilObjSAHSLearningModule::_lookupSubType($obj_id); $this->form = new ilPropertyFormGUI(); @@ -467,6 +478,13 @@ public function newModuleVersionUpload(): void $rbacsystem = $DIC->access(); $ilErr = $DIC["ilErr"]; + // a new module version replaces the IRSS container content, only + // possible once the module has been migrated to the Resource Storage + if (!$this->object->hasContainerResource()) { + $this->newModuleVersion(); + return; + } + $unzip = PATH_TO_UNZIP; $tocheck = "imsmanifest.xml"; @@ -518,8 +536,8 @@ public function newModuleVersionUpload(): void //remove temp file unlink($tmp_file); - //get old manifest file - $old_manifest = file_get_contents($this->object->getDataDirectory() . "/" . $tocheck); + //get old manifest file from the container resource + $old_manifest = (string) $this->object->getManifestFromContainer($tocheck); //reload fixed version of file $check = '/xmlns="http:\/\/www.imsglobal.org\/xsd\/imscp_v1p1"/'; @@ -533,28 +551,15 @@ public function newModuleVersionUpload(): void //get exisiting module version $module_version = $this->object->getModuleVersion() + 1; - if ($_FILES["scormfile"]["name"]) { - //build targetdir in lm_data - $file_path = $this->object->getDataDirectory() . "/" . $_FILES["scormfile"]["name"] . "." . $module_version; - $file_path = str_replace(".zip." . $module_version, "." . $module_version . ".zip", $file_path); - //move to data directory and add subfix for versioning - ilFileUtils::moveUploadedFile( - $_FILES["scormfile"]["tmp_name"], - $_FILES["scormfile"]["name"], - $file_path - ); - } else { - //build targetdir in lm_data - $uploadedFile = $DIC->http()->wrapper()->post()->retrieve('uploaded_file', $DIC->refinery()->kindlyTo()->string()); - $file_path = $this->object->getDataDirectory() . "/" . $uploadedFile . "." . $module_version; - $file_path = str_replace(".zip." . $module_version, "." . $module_version . ".zip", $file_path); - // move the already copied file to the lm_data directory - ilFileUtils::rename($source, $file_path); - } + // store the uploaded package as a new revision of the container + $revision_title = ($_FILES["scormfile"]["name"] ?? '') !== '' + ? (string) $_FILES["scormfile"]["name"] + : basename($source); + $this->object->replaceContainerFromZipPath($source, $revision_title); - //unzip and replace old extracted files - $DIC->legacyArchives()->unzip($file_path, null, true); - ilFileUtils::renameExecutables($this->object->getDataDirectory()); //(security) + if (isset($source_is_copy)) { + @unlink($source); + } //increase module version $this->object->setModuleVersion($module_version); diff --git a/components/ILIAS/ScormAicc/classes/class.ilSAHSStakeholder.php b/components/ILIAS/ScormAicc/classes/class.ilSAHSStakeholder.php new file mode 100644 index 000000000000..b7f6d636168c --- /dev/null +++ b/components/ILIAS/ScormAicc/classes/class.ilSAHSStakeholder.php @@ -0,0 +1,79 @@ + + */ +class ilSAHSStakeholder extends AbstractResourceStakeholder +{ + public function getId(): string + { + return 'sahs'; + } + + public function getOwnerOfNewResources(): int + { + return $this->default_owner; + } + + public function getLocationURIForResourceUsage(ResourceIdentification $identification): ?string + { + $db = $this->resolveDB(); + if ($db === null) { + return null; + } + $res = $db->queryF( + 'SELECT ref_id FROM sahs_lm + JOIN object_reference ON sahs_lm.id = object_reference.obj_id + WHERE rid = %s', + ['text'], + [$identification->serialize()] + ); + if ($row = $db->fetchAssoc($res)) { + return ilLink::_getStaticLink((int) $row['ref_id']); + } + return null; + } + + public function isResourceInUse(ResourceIdentification $identification): bool + { + $db = $this->resolveDB(); + if ($db === null) { + return true; // we assume it is in use + } + $res = $db->queryF( + 'SELECT id FROM sahs_lm WHERE rid = %s', + ['text'], + [$identification->serialize()] + ); + return $db->numRows($res) > 0; + } + + private function resolveDB(): ?ilDBInterface + { + global $DIC; + if ($DIC->isDependencyAvailable('database')) { + return $DIC->database(); + } + return null; + } +} diff --git a/components/ILIAS/ScormAicc/classes/class.ilScormAiccDataSet.php b/components/ILIAS/ScormAicc/classes/class.ilScormAiccDataSet.php index 295e6103323f..4af9f4eb1c46 100755 --- a/components/ILIAS/ScormAicc/classes/class.ilScormAiccDataSet.php +++ b/components/ILIAS/ScormAicc/classes/class.ilScormAiccDataSet.php @@ -187,14 +187,12 @@ public function getExtendedXmlRepresentation( 'scormFile' => "content.zip" ]; - // build content zip file + // build content zip file (sources from the container for migrated modules) if (isset($this->_archive['files']['scormFile'])) { - $lmDir = './' . ILIAS_WEB_DIR . "/" . CLIENT_ID . "/lm_data/lm_" . $id; + $lm = new ilObjSAHSLearningModule($id, false); // Important: content zip should not contain a 'lm_x' directory - $DIC->legacyArchives()->zip( - $lmDir, - $exportArchiveDir . "/" . $this->_archive['files']['scormFile'], - false + $lm->writeContentZip( + $exportArchiveDir . "/" . $this->_archive['files']['scormFile'] ); } diff --git a/components/ILIAS/ScormAicc/classes/class.ilScormAiccImporter.php b/components/ILIAS/ScormAicc/classes/class.ilScormAiccImporter.php index a2e0c35903e3..257de428fc1d 100755 --- a/components/ILIAS/ScormAicc/classes/class.ilScormAiccImporter.php +++ b/components/ILIAS/ScormAicc/classes/class.ilScormAiccImporter.php @@ -217,6 +217,8 @@ function (SimpleXMLElement $properties_xml_doc): ?\ILIAS\Data\Result { } $title = $new_object->readObject(); + // move the imported package into a Resource Storage container + $new_object->moveDataDirectoryToContainer(); $new_object->setLearningProgressSettingsAtUpload(); } diff --git a/components/ILIAS/ScormAicc/src/Setup/ScormAiccDatabaseUpdateSteps.php b/components/ILIAS/ScormAicc/src/Setup/ScormAiccDatabaseUpdateSteps.php new file mode 100644 index 000000000000..b378cc71ebef --- /dev/null +++ b/components/ILIAS/ScormAicc/src/Setup/ScormAiccDatabaseUpdateSteps.php @@ -0,0 +1,47 @@ +db = $db; + } + + /** + * Add the rid column to sahs_lm (Resource Storage migration of + * SCORM/AICC learning module content). + */ + public function step_1(): void + { + if ($this->db->tableExists('sahs_lm') && !$this->db->tableColumnExists('sahs_lm', 'rid')) { + $this->db->addTableColumn('sahs_lm', 'rid', [ + 'type' => 'text', + 'length' => 64, + 'notnull' => true, + 'default' => '' + ]); + } + } +} diff --git a/lang/ilias_en.lang b/lang/ilias_en.lang index 2b3a97882f0e..11496d5d6473 100644 --- a/lang/ilias_en.lang +++ b/lang/ilias_en.lang @@ -6703,6 +6703,7 @@ content#:#cont_linked_mobs#:#Linked media objects content#:#cont_links#:#Links content#:#cont_list_files#:#List Files content#:#cont_list_indent#:#Indent List +content#:#infobox_files_not_migrated#:#The files of this learning module have not yet been migrated to the Resource Storage Service and can therefore not be edited. content#:#cont_list_item_style#:#List Item Style content#:#cont_list_outdent#:#Outdent List content#:#cont_list_properties#:#List Properties