From 212aba2b143bcbfd254d7dfa9ad1d164b989c2c9 Mon Sep 17 00:00:00 2001 From: Christian Einvik Date: Wed, 29 Oct 2025 13:38:26 +0100 Subject: [PATCH 1/4] GH #3162: CA: Transfer content type machine name and title. Hub: Option and commands to switch between using content type machine name or title --- sourcecode/apis/contentauthor/app/Article.php | 7 -- sourcecode/apis/contentauthor/app/Content.php | 6 + sourcecode/apis/contentauthor/app/Game.php | 7 -- .../apis/contentauthor/app/H5PContent.php | 12 +- .../Controllers/API/H5PLibraryController.php | 31 +++++ .../Controllers/ReturnToCoreController.php | 2 + .../apis/contentauthor/app/Http/Kernel.php | 2 + .../app/Http/Middleware/AuthPsk.php | 25 ++++ .../app/Libraries/DataObjects/LtiContent.php | 1 + sourcecode/apis/contentauthor/app/Link.php | 7 -- .../app/Providers/RouteServiceProvider.php | 11 ++ .../apis/contentauthor/app/QuestionSet.php | 7 -- sourcecode/apis/contentauthor/routes/rest.php | 7 ++ .../Commands/H5PAttachContentTypeTitle.php | 111 ++++++++++++++++++ .../app/Console/Commands/H5PDisplayName.php | 40 +++++++ .../Console/Commands/SearchIndexRebuild.php | 24 ++++ .../hub/app/Jobs/SwapH5PTypeDisplayName.php | 54 +++++++++ sourcecode/hub/app/Models/Content.php | 39 +++++- sourcecode/hub/app/Models/ContentVersion.php | 2 +- sourcecode/hub/config/features.php | 2 + 20 files changed, 356 insertions(+), 41 deletions(-) create mode 100644 sourcecode/apis/contentauthor/app/Http/Middleware/AuthPsk.php create mode 100644 sourcecode/apis/contentauthor/routes/rest.php create mode 100644 sourcecode/hub/app/Console/Commands/H5PAttachContentTypeTitle.php create mode 100644 sourcecode/hub/app/Console/Commands/H5PDisplayName.php create mode 100644 sourcecode/hub/app/Console/Commands/SearchIndexRebuild.php create mode 100644 sourcecode/hub/app/Jobs/SwapH5PTypeDisplayName.php diff --git a/sourcecode/apis/contentauthor/app/Article.php b/sourcecode/apis/contentauthor/app/Article.php index 0d5b9549e6..fb9cee7806 100644 --- a/sourcecode/apis/contentauthor/app/Article.php +++ b/sourcecode/apis/contentauthor/app/Article.php @@ -301,11 +301,4 @@ private static function rewriteUploadUrls(string $content): string libxml_use_internal_errors($previous); } } - - protected function getTags(): array - { - return [ - 'h5p:' . $this->getMachineName(), - ]; - } } diff --git a/sourcecode/apis/contentauthor/app/Content.php b/sourcecode/apis/contentauthor/app/Content.php index 47076974ab..d7d155c415 100644 --- a/sourcecode/apis/contentauthor/app/Content.php +++ b/sourcecode/apis/contentauthor/app/Content.php @@ -88,6 +88,11 @@ abstract public function getUrl(): string; abstract public function getMachineName(): string; + public function getMachineDisplayName(): string + { + return $this->getMachineName(); + } + public function getTitleCleanAttribute(): string|null { if ($this->title === null) { @@ -368,6 +373,7 @@ public function toLtiContent( url: $this->getUrl(), title: $this->title_clean, machineName: $this->getMachineName(), + machineDisplayName: $this->getMachineDisplayName(), hasScore: ($this->getMaxScore() ?? 0) > 0, editUrl: $this->getEditUrl(), titleHtml: $this->title, diff --git a/sourcecode/apis/contentauthor/app/Game.php b/sourcecode/apis/contentauthor/app/Game.php index 5cdd6159f8..190cbc5bb3 100644 --- a/sourcecode/apis/contentauthor/app/Game.php +++ b/sourcecode/apis/contentauthor/app/Game.php @@ -140,13 +140,6 @@ public function getMachineName(): string return 'Game'; } - protected function getTags(): array - { - return [ - 'h5p:' . $this->getMachineName(), - ]; - } - public function getMaxScore(): int|null { try { diff --git a/sourcecode/apis/contentauthor/app/H5PContent.php b/sourcecode/apis/contentauthor/app/H5PContent.php index f93ca9e3ce..944d37071d 100644 --- a/sourcecode/apis/contentauthor/app/H5PContent.php +++ b/sourcecode/apis/contentauthor/app/H5PContent.php @@ -338,6 +338,11 @@ public function getMachineName(): string return $this->library()->firstOrFail()->name; } + public function getMachineDisplayName(): string + { + return $this->library()->first()->title ?? $this->getMachineName(); + } + public function getCopyrightCacheKey(): string { return 'h5p-copyright-' . $this->id; @@ -352,11 +357,4 @@ protected function getIconUrl(): string { return $this->library()->firstOrFail()->getIconUrl(); } - - protected function getTags(): array - { - return [ - 'h5p:' . $this->getMachineName(), - ]; - } } diff --git a/sourcecode/apis/contentauthor/app/Http/Controllers/API/H5PLibraryController.php b/sourcecode/apis/contentauthor/app/Http/Controllers/API/H5PLibraryController.php index bdb48d5619..7a552d0f58 100644 --- a/sourcecode/apis/contentauthor/app/Http/Controllers/API/H5PLibraryController.php +++ b/sourcecode/apis/contentauthor/app/Http/Controllers/API/H5PLibraryController.php @@ -5,6 +5,10 @@ use App\H5PLibrary; use App\Http\Controllers\Controller; use H5PCore; +use Illuminate\Http\JsonResponse; +use Illuminate\Http\Request; +use Illuminate\Http\Response; +use Illuminate\Support\Facades\DB; use Illuminate\Validation\ValidationException; class H5PLibraryController extends Controller @@ -29,4 +33,31 @@ public function getLibraryById($id) return response($h5pLibrary); } + + public function getLibraryTitleByMachineName(Request $request): Response|JsonResponse + { + $request->validate([ + "machineNames" => "required:array", + "machineNames.*" => "string", + ]); + $machineNames = $request->get('machineNames'); + // Get the titles, we get 'title' for all versions, but the latest is first for each 'name' (machine-name) + $info = H5PLibrary::select(DB::raw('LOWER(name) as name'), 'title', 'major_version', 'minor_version') + ->whereIn('name', $machineNames) + ->orderBy('name', 'ASC') + ->orderBy('major_version', 'DESC') + ->orderBy('minor_version', 'DESC') + ->get() + ->toArray(); + + $result = array_reduce($info, function ($result, $item) { + $key = $item['name']; + if (!isset($result[$key])) { + $result[$key] = $item['title']; + } + return $result; + }); + + return new JsonResponse($result, 200); + } } diff --git a/sourcecode/apis/contentauthor/app/Http/Controllers/ReturnToCoreController.php b/sourcecode/apis/contentauthor/app/Http/Controllers/ReturnToCoreController.php index 348d077c00..73264f91dd 100644 --- a/sourcecode/apis/contentauthor/app/Http/Controllers/ReturnToCoreController.php +++ b/sourcecode/apis/contentauthor/app/Http/Controllers/ReturnToCoreController.php @@ -54,6 +54,8 @@ public function __invoke(Request $request): Response ->withPublished($content->published) ->withShared($content->shared) ->withTags($content->tags) + ->withContentType($content->machineName) + ->withContentTypeName($content->machineDisplayName) ; $returnRequest = new Oauth1Request('POST', $ltiRequest->getReturnUrl(), [ diff --git a/sourcecode/apis/contentauthor/app/Http/Kernel.php b/sourcecode/apis/contentauthor/app/Http/Kernel.php index cf2ad045e7..122913827c 100644 --- a/sourcecode/apis/contentauthor/app/Http/Kernel.php +++ b/sourcecode/apis/contentauthor/app/Http/Kernel.php @@ -2,6 +2,7 @@ namespace App\Http; +use App\Http\Middleware\AuthPsk; use App\Http\Middleware\RequestId; use App\Http\Middleware\AdapterMode; use App\Http\Middleware\APIAuth; @@ -60,6 +61,7 @@ class Kernel extends HttpKernel protected $routeMiddleware = [ 'auth' => \App\Http\Middleware\Authenticate::class, 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, + 'auth.psk' => AuthPsk::class, 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 'can' => \Illuminate\Auth\Middleware\Authorize::class, diff --git a/sourcecode/apis/contentauthor/app/Http/Middleware/AuthPsk.php b/sourcecode/apis/contentauthor/app/Http/Middleware/AuthPsk.php new file mode 100644 index 0000000000..22f005b6c9 --- /dev/null +++ b/sourcecode/apis/contentauthor/app/Http/Middleware/AuthPsk.php @@ -0,0 +1,25 @@ +header($headerField); + $preKey = env($pskEnvName); + + if ($reqKey && $preKey && $reqKey === $preKey) { + return $next($request); + } + + return response("Unauthorized", Response::HTTP_UNAUTHORIZED); + } +} diff --git a/sourcecode/apis/contentauthor/app/Libraries/DataObjects/LtiContent.php b/sourcecode/apis/contentauthor/app/Libraries/DataObjects/LtiContent.php index 827b232a50..9a9e4801a1 100644 --- a/sourcecode/apis/contentauthor/app/Libraries/DataObjects/LtiContent.php +++ b/sourcecode/apis/contentauthor/app/Libraries/DataObjects/LtiContent.php @@ -17,6 +17,7 @@ public function __construct( public string $url, public string $title, public string $machineName, + public string $machineDisplayName, public bool $hasScore, public string|null $titleHtml = null, public string|null $editUrl = null, diff --git a/sourcecode/apis/contentauthor/app/Link.php b/sourcecode/apis/contentauthor/app/Link.php index 4024fdd83f..a02faf989c 100644 --- a/sourcecode/apis/contentauthor/app/Link.php +++ b/sourcecode/apis/contentauthor/app/Link.php @@ -113,11 +113,4 @@ public function getMachineName(): string { return 'Link'; } - - protected function getTags(): array - { - return [ - 'h5p:' . $this->getMachineName(), - ]; - } } diff --git a/sourcecode/apis/contentauthor/app/Providers/RouteServiceProvider.php b/sourcecode/apis/contentauthor/app/Providers/RouteServiceProvider.php index c281c7bba8..00a6d2a1e4 100644 --- a/sourcecode/apis/contentauthor/app/Providers/RouteServiceProvider.php +++ b/sourcecode/apis/contentauthor/app/Providers/RouteServiceProvider.php @@ -26,6 +26,7 @@ public function boot() */ public function map() { + $this->mapRestRoutes(); $this->mapApiRoutes(); $this->mapWebRoutes(); $this->mapAdminRoutes(); @@ -74,4 +75,14 @@ protected function mapAdminRoutes() require base_path('routes/admin.php'); }); } + + protected function mapRestRoutes() + { + Route::group([ + 'middleware' => 'api', + 'namespace' => $this->namespace, + ], function ($router) { + require base_path('routes/rest.php'); + }); + } } diff --git a/sourcecode/apis/contentauthor/app/QuestionSet.php b/sourcecode/apis/contentauthor/app/QuestionSet.php index 9efef44d92..0d146692d1 100644 --- a/sourcecode/apis/contentauthor/app/QuestionSet.php +++ b/sourcecode/apis/contentauthor/app/QuestionSet.php @@ -85,11 +85,4 @@ public function getMachineName(): string { return 'QuestionSet'; } - - protected function getTags(): array - { - return [ - 'h5p:' . $this->getMachineName(), - ]; - } } diff --git a/sourcecode/apis/contentauthor/routes/rest.php b/sourcecode/apis/contentauthor/routes/rest.php new file mode 100644 index 0000000000..12c71e4243 --- /dev/null +++ b/sourcecode/apis/contentauthor/routes/rest.php @@ -0,0 +1,7 @@ +middleware(['auth.psk:X-PSK,LTI_CONSUMER_KEY']); diff --git a/sourcecode/hub/app/Console/Commands/H5PAttachContentTypeTitle.php b/sourcecode/hub/app/Console/Commands/H5PAttachContentTypeTitle.php new file mode 100644 index 0000000000..2150120bd4 --- /dev/null +++ b/sourcecode/hub/app/Console/Commands/H5PAttachContentTypeTitle.php @@ -0,0 +1,111 @@ +choice( + 'Select the LTI tool to fetch the missing H5P content type data from, only content connected to the selected tool are updated', + LtiTool::select(['id', 'name'])->pluck('name', 'id')->toArray(), + attempts: 1, + multiple: false + ); + + $resourceCount = ContentVersion::where('lti_tool_id', $selectedTool)->count(); + if ($resourceCount === 0) { + $this->info('Selected tools do not have any content versions.'); + return; + } + + $this->info('Number of content versions connected to the selected tool: ' . $resourceCount . ''); + + // 1. Get the H5P machine names used by the content + $tags = Tag::select('tags.id', 'tags.name') + ->where('prefix', '=', 'h5p') + ->join('content_version_tag', 'content_version_tag.tag_id', '=', 'tags.id') + ->join('content_versions', 'content_versions.id', '=', 'content_version_tag.content_version_id') + ->where('content_versions.lti_tool_id', $selectedTool) + ->distinct() + ->get() + ->toArray(); + + $this->info('Unique machine names found: ' . count($tags) . ''); + + // 2. Fetch the title for the machine names and create as tag + $tool = LtiTool::findorFail($selectedTool); + $urlParts = parse_url($tool->creator_launch_url); + $url = "{$urlParts['scheme']}://{$urlParts['host']}/v1/h5p/library/title"; + $this->info("Querying {$urlParts['host']} for content type titles"); + + try { + $client = new Client(); + $response = $client->request('POST', $url, [ + RequestOptions::HEADERS => [ + 'X-PSK' => $tool->consumer_key, + ], + RequestOptions::JSON => (object)["machineNames" => collect($tags)->pluck('name')->toArray()], + ]); + + $titles = json_decode($response->getBody()->getContents(), true); + $this->info('Titles received: ' . count($titles) . ''); + $totalCount = 0; + foreach ($tags as $tag) { + $this->output->write("Processing tag '{$tag['name']}'"); + $title = $titles[$tag['name']] ?? $tag['name']; + + $this->output->write(", title '$title': "); + $title_tag = Tag::firstOrCreate([ + 'prefix' => 'h5p_title', + 'name' => $title, + ]); + + // 3. Connect the title tags to the content versions + $count = 0; + $changeCount = 0; + ContentVersion::whereIn('id', function ($query) use ($tag) { + $query->select('content_version_id') + ->from('content_version_tag') + ->where('tag_id', '=', $tag['id']); + }) + ->where('lti_tool_id', '=', $selectedTool) + ->chunkById(100, function (Collection $contentVersions) use ($title_tag, &$count, &$changeCount) { + $count += $contentVersions->count(); + $this->output->write("."); + $contentVersions->each(function (ContentVersion $contentVersion) use ($title_tag, &$changeCount) { + if ($contentVersion->tags()->where('id', $title_tag->id)->doesntExist()) { + $changeCount ++; + $contentVersion->tags()->attach( + $title_tag->id, + ['verbatim_name' => $title_tag->name] + ); + } + }); + }); + $totalCount += $count; + $this->output->write(" Created new tag for $changeCount of total $count content versions", newline: true); + } + $this->output->write("Total content versions processed: $totalCount", newline: true); + } catch (ClientException $e) { + $this->error($e->getMessage()); + return; + } + } +} diff --git a/sourcecode/hub/app/Console/Commands/H5PDisplayName.php b/sourcecode/hub/app/Console/Commands/H5PDisplayName.php new file mode 100644 index 0000000000..99c51649a1 --- /dev/null +++ b/sourcecode/hub/app/Console/Commands/H5PDisplayName.php @@ -0,0 +1,40 @@ +output->write("Configured value is: " . ($displaytype ? "$displaytype" : " - Not set -"), newline: true); + match($displaytype) { + 'h5p' => $this->info('Content type machine name is displayed as content type'), + 'h5p_title' => $this->info('Content type title is displayed as content type'), + default => $this->info('Value is unset or invalid. Default is displaying content type machine name as content type'), + }; + + if ($this->option('migrate')) { + if ($this->option('queue')) { + $dispatcher->dispatch(new \App\Jobs\SwapH5PTypeDisplayName()); + $this->info('The migration has been queued'); + + return; + } + + $this->info('Migrating content type display value...'); + $dispatcher->dispatchSync(new \App\Jobs\SwapH5PTypeDisplayName()); + $this->info('Done.'); + } + } +} diff --git a/sourcecode/hub/app/Console/Commands/SearchIndexRebuild.php b/sourcecode/hub/app/Console/Commands/SearchIndexRebuild.php new file mode 100644 index 0000000000..09c7574ddc --- /dev/null +++ b/sourcecode/hub/app/Console/Commands/SearchIndexRebuild.php @@ -0,0 +1,24 @@ +option('queue')) { + $dispatcher->dispatch(new RebuildContentIndex()); + $this->info('The Meilisearch rebuild has been queued'); + } else { + $this->info('Rebuilding Meilisearch index...'); + $dispatcher->dispatchSync(new RebuildContentIndex()); + } + } +} diff --git a/sourcecode/hub/app/Jobs/SwapH5PTypeDisplayName.php b/sourcecode/hub/app/Jobs/SwapH5PTypeDisplayName.php new file mode 100644 index 0000000000..8632483a6a --- /dev/null +++ b/sourcecode/hub/app/Jobs/SwapH5PTypeDisplayName.php @@ -0,0 +1,54 @@ +fail('SwapH5PTypeDisplayName job failed, invalid config value: ' . $displayType); + } else { + $this->updateDisplayedContentType($displayType); + } + } + + private function updateDisplayedContentType(string $prefix): void + { + DB::update(<<id; $duration = config('cache.content_versions.duration'); - + return Cache::remember($cacheKey, $duration, function () { return $this->latestVersion()->first(); }); @@ -224,7 +224,7 @@ public function getCachedLatestDraftVersion(): ?ContentVersion $cacheKey = config('cache.content_versions.latest_draft_version_key') . $this->id; $duration = config('cache.content_versions.duration'); - + return Cache::remember($cacheKey, $duration, function () { return $this->latestDraftVersion()->first(); }); @@ -241,7 +241,7 @@ public function getCachedLatestPublishedVersion(): ?ContentVersion $cacheKey = config('cache.content_versions.latest_published_version_key') . $this->id; $duration = config('cache.content_versions.duration'); - + return Cache::remember($cacheKey, $duration, function () { return $this->latestPublishedVersion()->first(); }); @@ -266,13 +266,42 @@ public function createVersionFromLinkItem( $version->editedBy()->associate($user); if ($item instanceof EdlibLtiLinkItem) { + $displayField = config('features.ca-content-type-display'); + $contentType = $item->getContentType(); // Content type machine name + $contentTypeName = $item->getContentTypeName(); // Content type title + $displayValue = ($displayField === 'h5p_title' ? $contentTypeName : $contentType); $version->published = $item->isPublished() ?? true; $version->language_iso_639_3 = strtolower($item->getLanguageIso639_3() ?? 'und'); $version->license = $item->getLicense(); $version->max_score = $item->getLineItem()?->getScoreConstraints()?->getTotalMaximum() ?? 0; + $version->displayed_content_type = $displayValue ?? $contentType ?? null; + + $version->saveQuietly(); + // Add content type info as tags + if ($contentType) { + $version->tags() + ->attach( + Tag::firstOrCreate([ + 'prefix' => 'h5p', + 'name' => strtolower($contentType), + ]), [ + 'verbatim_name' => strtolower($contentType), + ] + ); + } + if ($contentTypeName) { + $version->tags() + ->attach( + Tag::firstOrCreate([ + 'prefix' => 'h5p_title', + 'name' => $contentTypeName, + ]), [ + 'verbatim_name' => $contentTypeName, + ] + ); + } if (count($item->getTags()) > 0) { - $version->saveQuietly(); $version->handleSerializedTags($item->getTags()); } } diff --git a/sourcecode/hub/app/Models/ContentVersion.php b/sourcecode/hub/app/Models/ContentVersion.php index 236c92155f..1d6f569b83 100755 --- a/sourcecode/hub/app/Models/ContentVersion.php +++ b/sourcecode/hub/app/Models/ContentVersion.php @@ -280,7 +280,7 @@ public function getSerializedTags(): array public function handleSerializedTags(array $tags): void { foreach ($tags as $tag) { - // TODO: dedicated LTI-DL field for this + // Could be used by REST API, not used by CA if (str_starts_with($tag, 'h5p:')) { $this->displayed_content_type = substr($tag, 4); } diff --git a/sourcecode/hub/config/features.php b/sourcecode/hub/config/features.php index e431471f58..54bc196745 100644 --- a/sourcecode/hub/config/features.php +++ b/sourcecode/hub/config/features.php @@ -7,4 +7,6 @@ 'forgot-password' => (bool) env('FEATURE_RESET_PASSWORD_ENABLED', true), 'noindex' => (bool) env('FEATURE_NOINDEX', false), 'social-users-are-verified' => (bool) env('FEATURE_SOCIAL_USERS_ARE_VERIFIED', false), + // How to display the H5P Content type: 'h5p' to use content type machine name, 'h5p_title' to use content type title + 'ca-content-type-display' => env('FEATURE_CA_CONTENT_TYPE_DISPLAY', 'h5p'), ]; From 49fb3c4738b8985892a6ef9560cb662c5cda331d Mon Sep 17 00:00:00 2001 From: Christian Einvik Date: Wed, 29 Oct 2025 15:37:25 +0100 Subject: [PATCH 2/4] Add confirmation prompt to edlib:search-index-rebuild command --- .../app/Console/Commands/SearchIndexRebuild.php | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/sourcecode/hub/app/Console/Commands/SearchIndexRebuild.php b/sourcecode/hub/app/Console/Commands/SearchIndexRebuild.php index 09c7574ddc..f761fb1459 100644 --- a/sourcecode/hub/app/Console/Commands/SearchIndexRebuild.php +++ b/sourcecode/hub/app/Console/Commands/SearchIndexRebuild.php @@ -13,12 +13,17 @@ class SearchIndexRebuild extends Command public function handle(Dispatcher $dispatcher): void { - if ($this->option('queue')) { - $dispatcher->dispatch(new RebuildContentIndex()); - $this->info('The Meilisearch rebuild has been queued'); + if ($this->confirm('While rebuilding the index, listings and search will not display any content! Continue?', false)) { + if ($this->option('queue')) { + $dispatcher->dispatch(new RebuildContentIndex()); + $this->info('The Meilisearch rebuild has been queued'); + } else { + $this->info('Rebuilding Meilisearch index...'); + $dispatcher->dispatchSync(new RebuildContentIndex()); + $this->info('Done'); + } } else { - $this->info('Rebuilding Meilisearch index...'); - $dispatcher->dispatchSync(new RebuildContentIndex()); + $this->info('Cancelled'); } } } From 6f961a9ef1bb735c8a4bcc286179b16d30e2be6c Mon Sep 17 00:00:00 2001 From: Christian Einvik Date: Thu, 30 Oct 2025 12:57:40 +0100 Subject: [PATCH 3/4] GH #3162: Update composer package 'cerpus/edlib-resource-kit' in Hub and CA --- sourcecode/apis/contentauthor/composer.lock | 8 ++++---- sourcecode/hub/composer.lock | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/sourcecode/apis/contentauthor/composer.lock b/sourcecode/apis/contentauthor/composer.lock index a67ee373b5..bb93566861 100644 --- a/sourcecode/apis/contentauthor/composer.lock +++ b/sourcecode/apis/contentauthor/composer.lock @@ -495,12 +495,12 @@ "source": { "type": "git", "url": "https://github.com/cerpus/php-edlib-resource-kit.git", - "reference": "46948ce41206d701fd1de8cfcae380dfcb41ccf1" + "reference": "7b56ee4f87d460e97b5a971f872555702e45bbdd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/cerpus/php-edlib-resource-kit/zipball/46948ce41206d701fd1de8cfcae380dfcb41ccf1", - "reference": "46948ce41206d701fd1de8cfcae380dfcb41ccf1", + "url": "https://api.github.com/repos/cerpus/php-edlib-resource-kit/zipball/7b56ee4f87d460e97b5a971f872555702e45bbdd", + "reference": "7b56ee4f87d460e97b5a971f872555702e45bbdd", "shasum": "" }, "require": { @@ -547,7 +547,7 @@ "issues": "https://github.com/cerpus/Edlib/issues", "source": "https://github.com/cerpus/php-edlib-resource-kit/tree/master" }, - "time": "2025-02-13T09:11:06+00:00" + "time": "2025-10-30T11:21:19+00:00" }, { "name": "cerpus/edlib-resource-kit-laravel", diff --git a/sourcecode/hub/composer.lock b/sourcecode/hub/composer.lock index 9f0e233159..aa1187c522 100644 --- a/sourcecode/hub/composer.lock +++ b/sourcecode/hub/composer.lock @@ -292,12 +292,12 @@ "source": { "type": "git", "url": "https://github.com/cerpus/php-edlib-resource-kit.git", - "reference": "46948ce41206d701fd1de8cfcae380dfcb41ccf1" + "reference": "7b56ee4f87d460e97b5a971f872555702e45bbdd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/cerpus/php-edlib-resource-kit/zipball/46948ce41206d701fd1de8cfcae380dfcb41ccf1", - "reference": "46948ce41206d701fd1de8cfcae380dfcb41ccf1", + "url": "https://api.github.com/repos/cerpus/php-edlib-resource-kit/zipball/7b56ee4f87d460e97b5a971f872555702e45bbdd", + "reference": "7b56ee4f87d460e97b5a971f872555702e45bbdd", "shasum": "" }, "require": { @@ -344,7 +344,7 @@ "issues": "https://github.com/cerpus/Edlib/issues", "source": "https://github.com/cerpus/php-edlib-resource-kit/tree/master" }, - "time": "2025-02-13T09:11:06+00:00" + "time": "2025-10-30T11:21:19+00:00" }, { "name": "cerpus/edlib-resource-kit-laravel", From c20914bae7ea5183e9cc41ef61c1d8580b3ca5b4 Mon Sep 17 00:00:00 2001 From: Christian Einvik Date: Thu, 30 Oct 2025 14:02:39 +0100 Subject: [PATCH 4/4] GH #3162: Fix PhpStan issues. Adjust selection of LTI Tool for 'edlib:h5p-attach-content-type-title' command --- .../Commands/H5PAttachContentTypeTitle.php | 35 ++++++++++++++----- sourcecode/hub/app/Models/Content.php | 5 ++- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/sourcecode/hub/app/Console/Commands/H5PAttachContentTypeTitle.php b/sourcecode/hub/app/Console/Commands/H5PAttachContentTypeTitle.php index 2150120bd4..8dcbd09a72 100644 --- a/sourcecode/hub/app/Console/Commands/H5PAttachContentTypeTitle.php +++ b/sourcecode/hub/app/Console/Commands/H5PAttachContentTypeTitle.php @@ -22,16 +22,35 @@ class H5PAttachContentTypeTitle extends Command public function handle(): void { - $selectedTool = $this->choice( - 'Select the LTI tool to fetch the missing H5P content type data from, only content connected to the selected tool are updated', - LtiTool::select(['id', 'name'])->pluck('name', 'id')->toArray(), - attempts: 1, - multiple: false - ); + $this->info('Select the LTI tool to fetch the missing H5P content type data from, only content connected to the selected tool are updated'); + $tools = LtiTool::select(['id', 'name'])->pluck('name', 'id')->toArray(); + if (count($tools) === 0) { + $this->info('No LTI Tools found'); + return; + }else if (count($tools) > 1) { + $selectedTool = $this->choice( + 'Available LTI Tools', + $tools, + attempts: 1, + multiple: false + ); + } else { + $selectedTool = array_key_first($tools); + if (!$this->confirm("Found one LTI Tool named '{$tools[$selectedTool]}', proceed with this?", false)) {; + return; + } + } + + $tool = LtiTool::findorFail($selectedTool); + $urlParts = parse_url($tool->creator_launch_url); + if (!is_array($urlParts) or !array_key_exists('scheme', $urlParts) or !array_key_exists('host', $urlParts)) { + $this->error("Failed to extract scheme and host from LTI Tool launch url: '{$tool->creator_launch_url}'"); + return; + } $resourceCount = ContentVersion::where('lti_tool_id', $selectedTool)->count(); if ($resourceCount === 0) { - $this->info('Selected tools do not have any content versions.'); + $this->info('Selected LTI Tool do not have any content versions.'); return; } @@ -50,8 +69,6 @@ public function handle(): void $this->info('Unique machine names found: ' . count($tags) . ''); // 2. Fetch the title for the machine names and create as tag - $tool = LtiTool::findorFail($selectedTool); - $urlParts = parse_url($tool->creator_launch_url); $url = "{$urlParts['scheme']}://{$urlParts['host']}/v1/h5p/library/title"; $this->info("Querying {$urlParts['host']} for content type titles"); diff --git a/sourcecode/hub/app/Models/Content.php b/sourcecode/hub/app/Models/Content.php index aca2df7b6f..efb889dff1 100644 --- a/sourcecode/hub/app/Models/Content.php +++ b/sourcecode/hub/app/Models/Content.php @@ -274,7 +274,10 @@ public function createVersionFromLinkItem( $version->language_iso_639_3 = strtolower($item->getLanguageIso639_3() ?? 'und'); $version->license = $item->getLicense(); $version->max_score = $item->getLineItem()?->getScoreConstraints()?->getTotalMaximum() ?? 0; - $version->displayed_content_type = $displayValue ?? $contentType ?? null; + $displayedType = $displayValue ?? $contentType ?? null; + if ($displayedType) { + $version->displayed_content_type = $displayedType; + } $version->saveQuietly(); // Add content type info as tags