diff options
author | devianl2 <devianleong@gmail.com> | 2022-09-21 14:29:32 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-09-21 14:29:32 +0800 |
commit | 587072a458c2d76410d13d12754bb64ad12abb8d (patch) | |
tree | 1a832393b54434cf37c5223e4bfcabd050731eae | |
parent | cdad6eec2d4845477b00353c3af97591f56b460a (diff) | |
parent | 1764d938b4c7136738577d452f201bc3078156e9 (diff) |
Merge pull request #21 from KhaledLela/main
Enhances from my working version.
-rw-r--r-- | config/scorm.php | 12 | ||||
-rw-r--r-- | database/migrations/create_scorm_tables.php.stub | 4 | ||||
-rw-r--r-- | src/Entity/Scorm.php | 13 | ||||
-rw-r--r-- | src/Manager/ScormDisk.php | 128 | ||||
-rw-r--r-- | src/Manager/ScormManager.php | 126 | ||||
-rw-r--r-- | src/Model/ScormModel.php | 10 | ||||
-rw-r--r-- | src/Model/ScormScoTrackingModel.php | 7 |
7 files changed, 231 insertions, 69 deletions
diff --git a/config/scorm.php b/config/scorm.php index 0330bc5..d228bcc 100644 --- a/config/scorm.php +++ b/config/scorm.php @@ -3,10 +3,11 @@ return [ 'table_names' => [ - 'user_table' => 'users', // user table name on main LMS app. - 'scorm_table' => 'scorm', - 'scorm_sco_table' => 'scorm_sco', - 'scorm_sco_tracking_table' => 'scorm_sco_tracking', + 'user_table' => 'users', // user table name on main LMS app. + 'resource_table' => 'resource', // resource table on LMS app. + 'scorm_table' => 'scorm', + 'scorm_sco_table' => 'scorm_sco', + 'scorm_sco_tracking_table' => 'scorm_sco_tracking', ], /** * Scorm directory. You may create a custom path in file system @@ -23,5 +24,6 @@ return [ * 'bucket' => env('AWS_SCORM_BUCKET'), * ], */ - 'disk' => 'local', + 'disk' => 'local', + 'archive' => 'local', ]; diff --git a/database/migrations/create_scorm_tables.php.stub b/database/migrations/create_scorm_tables.php.stub index 496aca4..1fa78f6 100644 --- a/database/migrations/create_scorm_tables.php.stub +++ b/database/migrations/create_scorm_tables.php.stub @@ -20,9 +20,9 @@ class CreateScormTables extends Migration } // scorm_model - Schema::create($tableNames['scorm_table'], function (Blueprint $table) { + Schema::create($tableNames['scorm_table'], function (Blueprint $table) use ($tableNames){ $table->bigIncrements('id'); - $table->nullableMorphs('resource'); + $table->morphs($tableNames['resource_table']); $table->string('title'); $table->string('origin_file')->nullable(); $table->string('version'); diff --git a/src/Entity/Scorm.php b/src/Entity/Scorm.php index 48fd711..2a59a29 100644 --- a/src/Entity/Scorm.php +++ b/src/Entity/Scorm.php @@ -3,6 +3,8 @@ namespace Peopleaps\Scorm\Entity; +use Peopleaps\Scorm\Model\ScormModel; + class Scorm { const SCORM_12 = 'scorm_12'; @@ -17,6 +19,17 @@ class Scorm public $scos; public $scoSerializer; + public static function fromModel(ScormModel $model) + { + $instance = new self(); + $instance->setId($model->id); + $instance->setUuid($model->uuid); + $instance->setTitle($model->title); + $instance->setVersion($model->version); + $instance->setEntryUrl($model->entryUrl); + return $instance; + } + /** * @return string */ diff --git a/src/Manager/ScormDisk.php b/src/Manager/ScormDisk.php index ef82d1c..a280142 100644 --- a/src/Manager/ScormDisk.php +++ b/src/Manager/ScormDisk.php @@ -2,60 +2,119 @@ namespace Peopleaps\Scorm\Manager; +use Exception; use Illuminate\Filesystem\FilesystemAdapter; +use Illuminate\Http\UploadedFile; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Storage; use Peopleaps\Scorm\Exception\StorageNotFoundException; -use ZipArchive; class ScormDisk { /** * Extract zip file into destination directory. * - * @param string $path Destination directory - * @param string $zipFilePath The path to the zip file. + * @param UploadedFile|string $file zip source + * @param string $path The path to the destination. * - * @return bool True on success, false on failure. + * @return bool true on success, false on failure. */ - public function unzip($file, $path) + function unzipper($file, $target_dir) { - $path = $this->cleanPath($path); - - $zipArchive = new ZipArchive(); - if ($zipArchive->open($file) !== true) { - return false; + $target_dir = $this->cleanPath($target_dir); + $unzipper = resolve(\ZipArchive::class); + if ($unzipper->open($file)) { + /** @var FilesystemAdapter $disk */ + $disk = $this->getDisk(); + for ($i = 0; $i < $unzipper->numFiles; ++$i) { + $zipEntryName = $unzipper->getNameIndex($i); + $destination = $this->join($target_dir, $this->cleanPath($zipEntryName)); + if ($this->isDirectory($zipEntryName)) { + $disk->createDir($destination); + continue; + } + $disk->putStream($destination, $unzipper->getStream($zipEntryName)); + } + return true; } + return false; + } - /** @var FilesystemAdapter $disk */ - $disk = $this->getDisk(); - $createDir = 'createDir'; - $putStream = 'putStream'; - - if (!method_exists($disk, $createDir)) { - $createDir = 'createDirectory'; - $putStream = 'writeStream'; + /** + * @param string $file SCORM archive uri on storage. + * @param callable $fn function run user stuff before unlink + */ + public function readScormArchive($file, callable $fn) + { + try { + if (Storage::exists($file)) { + Storage::delete($file); + } + Storage::writeStream($file, $this->getArchiveDisk()->readStream($file)); + $path = Storage::path($file); + call_user_func($fn, $path); + // Clean local resources + $this->clean($file); + } catch (Exception $ex) { + Log::error($ex->getMessage()); + throw new StorageNotFoundException('scorm_archive_not_found'); } + } - for ($i = 0; $i < $zipArchive->numFiles; ++$i) { - $zipEntryName = $zipArchive->getNameIndex($i); - $destination = $path . DIRECTORY_SEPARATOR . $this->cleanPath($zipEntryName); - if ($this->isDirectory($zipEntryName)) { - $disk->$createDir($destination); - continue; - } - $disk->$putStream($destination, $zipArchive->getStream($zipEntryName)); + private function clean($file) + { + try { + Storage::delete($file); + Storage::deleteDirectory(dirname($file)); // delete temp dir + } catch (Exception $ex) { + Log::error($ex->getMessage()); } + } + + /** + * @param string $directory + * @return bool + */ + public function deleteScorm($uuid) + { + $this->deleteScormArchive($uuid); // try to delete archive if exists. + return $this->deleteScormContent($uuid); + } - return true; + /** + * @param string $directory + * @return bool + */ + private function deleteScormContent($folderHashedName) + { + try { + return $this->getDisk()->deleteDirectory($folderHashedName); + } catch (Exception $ex) { + Log::error($ex->getMessage()); + } } /** * @param string $directory * @return bool */ - public function deleteScormFolder($folderHashedName) + private function deleteScormArchive($uuid) { - return $this->getDisk()->deleteDirectory($folderHashedName); + try { + return $this->getArchiveDisk()->deleteDirectory($uuid); + } catch (Exception $ex) { + Log::error($ex->getMessage()); + } + } + + /** + * + * @param array $paths + * @return string joined path + */ + private function join(...$paths) + { + return implode(DIRECTORY_SEPARATOR, $paths); } private function isDirectory($zipEntryName) @@ -78,4 +137,15 @@ class ScormDisk } return Storage::disk(config('scorm.disk')); } + + /** + * @return FilesystemAdapter $disk + */ + private function getArchiveDisk() + { + if (!config()->has('filesystems.disks.' . config('scorm.archive'))) { + throw new StorageNotFoundException('scorm_archive_disk_not_define'); + } + return Storage::disk(config('scorm.archive')); + } } diff --git a/src/Manager/ScormManager.php b/src/Manager/ScormManager.php index 1d40005..c804385 100644 --- a/src/Manager/ScormManager.php +++ b/src/Manager/ScormManager.php @@ -17,7 +17,6 @@ use Peopleaps\Scorm\Model\ScormScoModel; use Peopleaps\Scorm\Model\ScormScoTrackingModel; use Illuminate\Support\Str; use Peopleaps\Scorm\Entity\Sco; -use ZipArchive; class ScormManager { @@ -25,6 +24,8 @@ class ScormManager private $scormLib; /** @var ScormDisk */ private $scormDisk; + /** @var string $uuid */ + private $uuid; /** * Constructor. @@ -38,31 +39,70 @@ class ScormManager $this->scormDisk = new ScormDisk(); } + public function uploadScormFromUri($file) + { + $scorm = null; + $this->scormDisk->readScormArchive($file, function ($path) use (&$scorm, $file) { + $this->uuid = dirname($file); + $filename = basename($file); + $scorm = $this->saveScorm($path, $filename); + }); + return $scorm; + } + public function uploadScormArchive(UploadedFile $file) { - // Checks if it is a valid scorm archive - $scormData = null; - $zip = new ZipArchive(); - $openValue = $zip->open($file); + $this->uuid = Str::uuid(); + return $this->saveScorm($file, $file->getClientOriginalName()); + } + /** + * Checks if it is a valid scorm archive + * + * @param string|UploadedFile $file zip. + */ + private function validatePackage($file) + { + $zip = new \ZipArchive(); + $openValue = $zip->open($file); $isScormArchive = (true === $openValue) && $zip->getStream('imsmanifest.xml'); $zip->close(); - if (!$isScormArchive) { - throw new InvalidScormArchiveException('invalid_scorm_archive_message'); - } else { - $scormData = $this->generateScorm($file); + $this->onError('invalid_scorm_archive_message'); } + } + /** + * Save scom data + * + * @param string|UploadedFile $file zip. + */ + private function saveScorm($file, $filename) + { + $this->validatePackage($file); + $scormData = $this->generateScorm($file); // save to db if (is_null($scormData) || !is_array($scormData)) { - throw new InvalidScormArchiveException('invalid_scorm_data'); + $this->onError('invalid_scorm_data'); } - // Magic method: https://laravel.com/docs/5.0/queries#advanced-wheres - // https://github.com/laravel/framework/blob/9.x/src/Illuminate/Database/Query/Builder.php - $scorm = ScormModel::whereOriginFile($scormData['identifier']); + /** + * ScormModel::whereOriginFile Query Builder style equals ScormModel::where('origin_file',$value) + * + * From Laravel doc https://laravel.com/docs/5.0/queries#advanced-wheres. + * Dynamic Where Clauses + * You may even use "dynamic" where statements to fluently build where statements using magic methods: + * + * Examples: + * + * $admin = DB::table('users')->whereId(1)->first(); + * From laravel framework https://github.com/laravel/framework/blob/9.x/src/Illuminate/Database/Query/Builder.php' + * Handle dynamic method calls into the method. + * return $this->dynamicWhere($method, $parameters); + **/ + $scorm = ScormModel::whereOriginFile($filename); + // Check if scom package already exists to drop old one. if (!$scorm->exists()) { $scorm = new ScormModel(); @@ -70,12 +110,12 @@ class ScormManager $scorm = $scorm->first(); $this->deleteScormData($scorm); } - $scorm->uuid = $scormData['uuid']; $scorm->title = $scormData['title']; $scorm->version = $scormData['version']; $scorm->entry_url = $scormData['entryUrl']; - $scorm->origin_file = $scormData['identifier']; + $scorm->identifier = $scormData['identifier']; + $scorm->origin_file = $filename; $scorm->save(); if (!empty($scormData['scos']) && is_array($scormData['scos'])) { @@ -122,7 +162,10 @@ class ScormManager return $sco; } - private function parseScormArchive(UploadedFile $file) + /** + * @param string|UploadedFile $file zip. + */ + private function parseScormArchive($file) { $data = []; $contents = ''; @@ -139,14 +182,14 @@ class ScormManager $dom = new DOMDocument(); if (!$dom->loadXML($contents)) { - throw new InvalidScormArchiveException('cannot_load_imsmanifest_message'); + $this->onError('cannot_load_imsmanifest_message'); } $manifest = $dom->getElementsByTagName('manifest')->item(0); if (!is_null($manifest->attributes->getNamedItem('identifier'))) { $data['identifier'] = $manifest->attributes->getNamedItem('identifier')->nodeValue; } else { - throw new InvalidScormArchiveException('invalid_scorm_manifest_identifier'); + $this->onError('invalid_scorm_manifest_identifier'); } $titles = $dom->getElementsByTagName('title'); if ($titles->length > 0) { @@ -165,15 +208,15 @@ class ScormManager $data['version'] = Scorm::SCORM_2004; break; default: - throw new InvalidScormArchiveException('invalid_scorm_version_message'); + $this->onError('invalid_scorm_version_message'); } } else { - throw new InvalidScormArchiveException('invalid_scorm_version_message'); + $this->onError('invalid_scorm_version_message'); } $scos = $this->scormLib->parseOrganizationsNode($dom); if (0 >= count($scos)) { - throw new InvalidScormArchiveException('no_sco_in_scorm_archive_message'); + $this->onError('no_sco_in_scorm_archive_message'); } $data['entryUrl'] = $scos[0]->entryUrl ?? $scos[0]->scoChildren[0]->entryUrl; @@ -211,28 +254,27 @@ class ScormManager */ protected function deleteScormFolder($folderHashedName) { - return $this->scormDisk->deleteScormFolder($folderHashedName); + return $this->scormDisk->deleteScorm($folderHashedName); } /** - * @param UploadedFile $file + * @param string|UploadedFile $file zip. * @return array * @throws InvalidScormArchiveException */ - private function generateScorm(UploadedFile $file) + private function generateScorm($file) { - $uuid = Str::uuid(); $scormData = $this->parseScormArchive($file); /** * Unzip a given ZIP file into the web resources directory. * * @param string $hashName name of the destination directory */ - $this->scormDisk->unzip($file, $uuid); + $this->scormDisk->unzipper($file, $this->uuid); return [ 'identifier' => $scormData['identifier'], - 'uuid' => $uuid, + 'uuid' => $this->uuid, 'title' => $scormData['title'], // to follow standard file data format 'version' => $scormData['version'], 'entryUrl' => $scormData['entryUrl'], @@ -262,9 +304,8 @@ class ScormManager */ public function getScoByUuid($scoUuid) { - $sco = ScormScoModel::with([ - 'scorm' - ])->where('uuid', $scoUuid) + $sco = ScormScoModel::with(['scorm']) + ->where('uuid', $scoUuid) ->firstOrFail(); return $sco; @@ -275,7 +316,7 @@ class ScormManager return ScormScoTrackingModel::where('sco_id', $scoId)->where('user_id', $userId)->first(); } - public function createScoTracking($scoUuid, $userId = null) + public function createScoTracking($scoUuid, $userId = null, $userName = null) { $sco = ScormScoModel::where('uuid', $scoUuid)->firstOrFail(); @@ -283,6 +324,7 @@ class ScormManager $scoTracking = new ScoTracking(); $scoTracking->setSco($sco->toArray()); + $cmi = null; switch ($version) { case Scorm::SCORM_12: $scoTracking->setLessonStatus('not attempted'); @@ -300,16 +342,29 @@ class ScormManager } else { $scoTracking->setIsLocked(true); } + $cmi = [ + 'cmi.core.entry' => $scoTracking->getEntry(), + 'cmi.core.student_id' => $userId, + 'cmi.core.student_name' => $userName, + ]; + break; case Scorm::SCORM_2004: $scoTracking->setTotalTimeString('PT0S'); $scoTracking->setCompletionStatus('unknown'); $scoTracking->setLessonStatus('unknown'); $scoTracking->setIsLocked(false); + $cmi = [ + 'cmi.entry' => 'ab-initio', + 'cmi.learner_id' => $userId, + 'cmi.learner_name' => $userName, + 'cmi.scaled_passing_score' => 0.5, + ]; break; } $scoTracking->setUserId($userId); + $scoTracking->setDetails($cmi); // Create a new tracking model $storeTracking = ScormScoTrackingModel::firstOrCreate([ @@ -652,4 +707,13 @@ class ScormManager return $formattedValue; } + + /** + * Clean resources and throw exception. + */ + private function onError($msg) + { + $this->scormDisk->deleteScorm($this->uuid); + throw new InvalidScormArchiveException($msg); + } } diff --git a/src/Model/ScormModel.php b/src/Model/ScormModel.php index 35b2d4e..e7c5a16 100644 --- a/src/Model/ScormModel.php +++ b/src/Model/ScormModel.php @@ -6,6 +6,13 @@ namespace Peopleaps\Scorm\Model; use Illuminate\Database\Eloquent\Model; +/** + * @property int $id + * @property string $uuid + * @property string $title + * @property string $version + * @property string $entryUrl + */ class ScormModel extends Model { @@ -41,7 +48,8 @@ class ScormModel extends Model return config('scorm.table_names.scorm_table', parent::getTable()); } - public function scos() { + public function scos() + { return $this->hasMany(ScormScoModel::class, 'scorm_id', 'id'); } } diff --git a/src/Model/ScormScoTrackingModel.php b/src/Model/ScormScoTrackingModel.php index 2c057c7..407e2da 100644 --- a/src/Model/ScormScoTrackingModel.php +++ b/src/Model/ScormScoTrackingModel.php @@ -35,12 +35,17 @@ class ScormScoTrackingModel extends Model 'updated_at' ]; + protected $casts = [ + 'details' => 'array', + ]; + public function getTable() { return config('scorm.table_names.scorm_sco_tracking_table', parent::getTable()); } - public function sco() { + public function sco() + { return $this->belongsTo(ScormScoModel::class, 'sco_id', 'id'); } } |