Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions sourcecode/apis/contentauthor/app/Article.php
Original file line number Diff line number Diff line change
Expand Up @@ -301,11 +301,4 @@ private static function rewriteUploadUrls(string $content): string
libxml_use_internal_errors($previous);
}
}

protected function getTags(): array
{
return [
'h5p:' . $this->getMachineName(),
];
}
}
6 changes: 6 additions & 0 deletions sourcecode/apis/contentauthor/app/Content.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
Expand Down
7 changes: 0 additions & 7 deletions sourcecode/apis/contentauthor/app/Game.php
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,6 @@ public function getMachineName(): string
return 'Game';
}

protected function getTags(): array
{
return [
'h5p:' . $this->getMachineName(),
];
}

public function getMaxScore(): int|null
{
try {
Expand Down
12 changes: 5 additions & 7 deletions sourcecode/apis/contentauthor/app/H5PContent.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -352,11 +357,4 @@ protected function getIconUrl(): string
{
return $this->library()->firstOrFail()->getIconUrl();
}

protected function getTags(): array
{
return [
'h5p:' . $this->getMachineName(),
];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(), [
Expand Down
2 changes: 2 additions & 0 deletions sourcecode/apis/contentauthor/app/Http/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
25 changes: 25 additions & 0 deletions sourcecode/apis/contentauthor/app/Http/Middleware/AuthPsk.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

// Verify that a pre-shared key in request header field $headerField matches key in environment variable $pskEnvName
class AuthPsk
{
public function handle(Request $request, Closure $next, $headerField, $pskEnvName)
{
$reqKey = $request->header($headerField);
$preKey = env($pskEnvName);

if ($reqKey && $preKey && $reqKey === $preKey) {
return $next($request);
}

return response("Unauthorized", Response::HTTP_UNAUTHORIZED);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 0 additions & 7 deletions sourcecode/apis/contentauthor/app/Link.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,4 @@ public function getMachineName(): string
{
return 'Link';
}

protected function getTags(): array
{
return [
'h5p:' . $this->getMachineName(),
];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public function boot()
*/
public function map()
{
$this->mapRestRoutes();
$this->mapApiRoutes();
$this->mapWebRoutes();
$this->mapAdminRoutes();
Expand Down Expand Up @@ -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');
});
}
}
7 changes: 0 additions & 7 deletions sourcecode/apis/contentauthor/app/QuestionSet.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,4 @@ public function getMachineName(): string
{
return 'QuestionSet';
}

protected function getTags(): array
{
return [
'h5p:' . $this->getMachineName(),
];
}
}
8 changes: 4 additions & 4 deletions sourcecode/apis/contentauthor/composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions sourcecode/apis/contentauthor/routes/rest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

use App\Http\Controllers\API\H5PLibraryController;

// Get the H5P library (content type) title for library machine-names
Route::post('v1/h5p/library/title', [H5PLibraryController::class, 'getLibraryTitleByMachineName'])
->middleware(['auth.psk:X-PSK,LTI_CONSUMER_KEY']);
128 changes: 128 additions & 0 deletions sourcecode/hub/app/Console/Commands/H5PAttachContentTypeTitle.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<?php

declare(strict_types=1);

namespace App\Console\Commands;

use App\Models\ContentVersion;
use App\Models\LtiTool;
use App\Models\Tag;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\RequestOptions;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Collection;

/**
* One-time job to fetch missing H5P content type titles from LTI Tool, create as tags and attach to content versions
*/
class H5PAttachContentTypeTitle extends Command
{
protected $signature = 'edlib:h5p-attach-content-type-title';

public function handle(): void
{
$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 LTI Tool do not have any content versions.');
return;
}

$this->info('Number of content versions connected to the selected tool: <comment>' . $resourceCount . '</comment>');

// 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: <comment>' . count($tags) . '</comment>');

// 2. Fetch the title for the machine names and create as tag
$url = "{$urlParts['scheme']}://{$urlParts['host']}/v1/h5p/library/title";
$this->info("Querying <comment>{$urlParts['host']}</comment> 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: <comment>' . count($titles) . '</comment>');
$totalCount = 0;
foreach ($tags as $tag) {
$this->output->write("<info>Processing tag</info> '<comment>{$tag['name']}</comment>'");
$title = $titles[$tag['name']] ?? $tag['name'];

$this->output->write("<info>, title</info> '<comment>$title</comment>': ");
$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 <comment>$changeCount</comment> of total <comment>$count</comment> content versions", newline: true);
}
$this->output->write("Total content versions processed: <comment>$totalCount</comment>", newline: true);
} catch (ClientException $e) {
$this->error($e->getMessage());
return;
}
}
}
Loading
Loading