summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordevianl2 <devianleong@gmail.com>2022-09-21 14:29:32 +0800
committerGitHub <noreply@github.com>2022-09-21 14:29:32 +0800
commit587072a458c2d76410d13d12754bb64ad12abb8d (patch)
tree1a832393b54434cf37c5223e4bfcabd050731eae
parentcdad6eec2d4845477b00353c3af97591f56b460a (diff)
parent1764d938b4c7136738577d452f201bc3078156e9 (diff)
Merge pull request #21 from KhaledLela/main
Enhances from my working version.
-rw-r--r--config/scorm.php12
-rw-r--r--database/migrations/create_scorm_tables.php.stub4
-rw-r--r--src/Entity/Scorm.php13
-rw-r--r--src/Manager/ScormDisk.php128
-rw-r--r--src/Manager/ScormManager.php126
-rw-r--r--src/Model/ScormModel.php10
-rw-r--r--src/Model/ScormScoTrackingModel.php7
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');
}
}