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
78 changes: 53 additions & 25 deletions assertions/route.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,29 @@
// assertions/.htaccess routes here directly, bypassing tsugi.php
LTIX::session_start();

/**
* Assertion pages are public credentials. The bar is the site name,
* opening the app home in a new tab, and Close. No course or build menu.
*/
function assertion_nav() {
global $CFG, $OUTPUT;
$home = (isset($CFG->apphome) && is_string($CFG->apphome) && $CFG->apphome !== '')
? $CFG->apphome
: (isset($CFG->wwwroot) && is_string($CFG->wwwroot) ? $CFG->wwwroot : '/');
$label = (isset($CFG->servicename) && is_string($CFG->servicename) && $CFG->servicename !== '')
? $CFG->servicename
: 'Home';
$set = new \Tsugi\UI\MenuSet();
$set->setHome(
htmlspecialchars($label, ENT_QUOTES, 'UTF-8'),
htmlspecialchars($home, ENT_QUOTES, 'UTF-8'),
'target="_blank" rel="noopener noreferrer"'
);
$set->addRight(__('Close'), 'javascript:window_close();');
$OUTPUT->menuOverride = $set;
$OUTPUT->topNav();
}

// Parse the URL to extract encrypted ID and resource type
$url = $_SERVER['REQUEST_URI'];
$path = trim(parse_url($url, PHP_URL_PATH), '/');
Expand All @@ -34,7 +57,7 @@
http_response_code(404);
$OUTPUT->header();
$OUTPUT->bodyStart();
$OUTPUT->topNav();
assertion_nav();
echo("<h2>Page not found.</h2>\n");
$OUTPUT->footer();
return;
Expand All @@ -49,7 +72,7 @@
http_response_code(400);
$OUTPUT->header();
$OUTPUT->bodyStart();
$OUTPUT->topNav();
assertion_nav();
echo("<h2>Bad Request</h2><p>Missing badge id.</p>\n");
$OUTPUT->footer();
return;
Expand All @@ -59,22 +82,22 @@
http_response_code(403);
$OUTPUT->header();
$OUTPUT->bodyStart();
$OUTPUT->topNav();
assertion_nav();
echo("<h2>Forbidden</h2><p>You must be logged in to publish a badge.</p>\n");
$OUTPUT->footer();
return;
}
if ( ! Manifest::hasCurrent() ) {
$PDOX = LTIX::getConnection();
$l = BadgeService::lessonsForAssertion($hex_id);
if ( ! $l ) {
die_with_error_log('Cannot find lessons.json ($CFG->lessons) or an active course manifest');
}
$PDOX = LTIX::getConnection();
$l = Manifest::requireCurrentLessons();
$parsed = Badges::parseAssertionId($hex_id, $l);
if ( is_string($parsed) ) {
http_response_code(404);
$OUTPUT->header();
$OUTPUT->bodyStart();
$OUTPUT->topNav();
assertion_nav();
echo("<h2>Badge not found</h2><p>Invalid or expired badge id.</p>\n");
$OUTPUT->footer();
return;
Expand All @@ -87,7 +110,7 @@
http_response_code(403);
$OUTPUT->header();
$OUTPUT->bodyStart();
$OUTPUT->topNav();
assertion_nav();
echo("<h2>Forbidden</h2><p>You can only publish your own badges.</p>\n");
$OUTPUT->footer();
return;
Expand All @@ -98,13 +121,13 @@
$email = U::get($row, 'email', '');
$context_title = U::get($row, 'title', '');
$badge_title = isset($badge->title) ? $badge->title : '';
$accomplishments = BadgeService::captureAccomplishments($l, $badge, is_string($context_title) ? $context_title : '');
$accomplishments = BadgeService::captureAccomplishments($l, $badge, is_string($context_title) ? $context_title : '', $context_id);
$guid = BadgeService::mintOrGet($current_user_id, $context_id, $code, $displayname, $email, $context_title, $badge_title, $accomplishments);
if ($guid === false) {
http_response_code(500);
$OUTPUT->header();
$OUTPUT->bodyStart();
$OUTPUT->topNav();
assertion_nav();
echo("<h2>Error</h2><p>Could not publish badge. Please try again later.</p>\n");
$OUTPUT->footer();
return;
Expand All @@ -122,7 +145,7 @@
http_response_code(404);
$OUTPUT->header();
$OUTPUT->bodyStart();
$OUTPUT->topNav();
assertion_nav();
echo("<h2>Not found</h2><p>Badge not found.</p>\n");
$OUTPUT->footer();
return;
Expand All @@ -132,7 +155,7 @@
http_response_code(404);
$OUTPUT->header();
$OUTPUT->bodyStart();
$OUTPUT->topNav();
assertion_nav();
echo("<h2>Not found</h2><p>LinkedIn sharing is not available for this badge.</p>\n");
$OUTPUT->footer();
return;
Expand Down Expand Up @@ -183,7 +206,7 @@
// Find the badge by code
$badge = null;
foreach($l->lessons->badges as $b) {
if ($b->image == $code.'.png') {
if (isset($b->image) && \Tsugi\Services\Badges\CourseBadges::imageMatchesCode($b->image, $code)) {
$badge = $b;
break;
}
Expand All @@ -193,7 +216,7 @@
http_response_code(404);
$OUTPUT->header();
$OUTPUT->bodyStart();
$OUTPUT->topNav();
assertion_nav();
echo("<h2>Badge not found.</h2>\n");
$OUTPUT->footer();
return;
Expand Down Expand Up @@ -275,7 +298,7 @@
http_response_code(404);
$OUTPUT->header();
$OUTPUT->bodyStart();
$OUTPUT->topNav();
assertion_nav();
echo("<h2>Page not found.</h2>\n");
$OUTPUT->footer();
return;
Expand All @@ -284,13 +307,13 @@
http_response_code(404);
$OUTPUT->header();
$OUTPUT->bodyStart();
$OUTPUT->topNav();
assertion_nav();
echo("<h2>Page not found.</h2>\n");
$OUTPUT->footer();
return;
}

// A published badge is the row. An unpublished hex id still needs the current lessons.
// A published badge is the row. An unpublished hex id names its own course.
$PDOX = LTIX::getConnection();

$row = null;
Expand All @@ -311,7 +334,11 @@
$badge = $data['badge'];
$frozen = $data['frozen'];
} else {
$l = Manifest::requireCurrentLessons();
$l = BadgeService::lessonsForAssertion($encrypted);
if ( ! $l ) {
error_log("Assertion parse error: badge not found for id: " . substr($encrypted, 0, 50) . "...");
die_with_error_log('Badge not found');
}
$x = Badges::parseAssertionId($encrypted, $l);
if ( ! is_string($x) ) {
$row = $x[0];
Expand Down Expand Up @@ -354,12 +381,13 @@
}
$badge_image = $live_image !== '' ? $live_image : $archive_image;
} else {
$badge_image = $CFG->badge_url.'/'.$code.'.png';
$badge_image = Badges::liveImageUrl($badge, $code, (int) $pieces[2]);
$evidence_home = isset($CFG->apphome) ? $CFG->apphome : '';
$evidence_assignments = BadgeService::getAssignmentsForEvidence($code, $l);
$claim_badge_url = $CFG->wwwroot . "/assertions/badge/" . urlencode($code) . ".json";
$claim_issuer_url = $CFG->wwwroot . "/assertions/issuer.json";
$claim_achievement_url = $CFG->wwwroot . "/assertions/badge/" . urlencode($code) . ".json?format=ob3";
$evidence_assignments = BadgeService::getAssignmentsForEvidence($code, $l, (int) $pieces[2]);
$guid_path = rtrim($CFG->wwwroot, '/') . '/assertions/' . urlencode($encrypted);
$claim_badge_url = $guid_path . '/badge.json';
$claim_issuer_url = $guid_path . '/issuer.json';
$claim_achievement_url = $guid_path . '/achievement.json';
$issuer_name_plain = null;
}

Expand Down Expand Up @@ -562,7 +590,7 @@
);
echo("\n");
$OUTPUT->bodyStart();
$OUTPUT->topNav();
assertion_nav();
?>
<style>
.badge-share-links { list-style: none; display: inline; margin: 0; padding: 0; }
Expand Down Expand Up @@ -780,7 +808,7 @@ function copyBadgeUrlToClipboard(button) {
http_response_code(404);
$OUTPUT->header();
$OUTPUT->bodyStart();
$OUTPUT->topNav();
assertion_nav();
echo("<h2>Page not found.</h2>\n");
$OUTPUT->footer();
break;
Expand Down
6 changes: 4 additions & 2 deletions badges/assert.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@
require_once "../config.php";
require_once "badge-util.php";

$l = Manifest::requireCurrentLessons();

$PDOX = LTIX::getConnection();

//echo("<pre>\n");
$encrypted = $_GET['id'];
$l = \Tsugi\Services\Badges\BadgeService::lessonsForAssertion($encrypted);
if ( ! $l ) {
die_with_error_log('Cannot find lessons.json ($CFG->lessons) or an active course manifest');
}
$x = parse_badge_id($encrypted, $l);
if ( is_string($x) ) {
if ( strpos($x, '<h2>Badge Configuration Required</h2>') !== false ) {
Expand Down
79 changes: 9 additions & 70 deletions badges/badge-util.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,82 +59,19 @@ function check_badge_config() {
}

function parse_badge_id($encrypted, $lesson) {
global $CFG, $PDOX;

$config_error = check_badge_config();
if ( $config_error !== false ) {
return $config_error;
}

$decrypted = \Tsugi\Crypt\AesCtr::decrypt(hex2bin($encrypted), $CFG->badge_encrypt_password, 256);
$pieces = explode(':',$decrypted);

if ( count($pieces) != 3 || !is_numeric($pieces[0]) || !is_numeric($pieces[2])) {
return 'Decryption failed';
}
$pattern = '/[^a-zA-Z0-9_]/';
if ( preg_match($pattern, $pieces[1]) ) {
return 'File pattern fail';
}

$badge = null;
if ( is_object($lesson) && isset($lesson->lessons->badges) && is_iterable($lesson->lessons->badges) ) {
foreach ( $lesson->lessons->badges as $candidate ) {
if ( isset($candidate->image) && $candidate->image == $pieces[1].'.png' ) {
$badge = $candidate;
break;
}
}
}
$minted = \Tsugi\Services\Badges\BadgeService::mintedDisplayRow(
(int) $pieces[0],
(string) $pieces[1],
(int) $pieces[2]
);
if ( $badge === null && $minted !== null ) {
$badge = \Tsugi\Services\Badges\BadgeService::syntheticBadge(
(string) $pieces[1],
$minted['badge_title']
);
}
if ( $badge === null ) {
return 'Bad badge image';
}

// Load the badge
$file = $CFG->badge_path . '/' . $pieces[1] . '.png';
$png = file_get_contents($file);
if ( $png === false ) return 'File contents fail';

// Join the course even when it is soft-deleted. Published badges stay valid.
$row = $PDOX->rowDie(
"SELECT displayname, email, user_key, U.login_at, title FROM {$CFG->dbprefix}lti_user AS U
JOIN {$CFG->dbprefix}lti_membership AS M
ON U.user_id = M.user_id
JOIN {$CFG->dbprefix}lti_context AS C
ON M.context_id = C.context_id
WHERE U.user_id = :UID AND M.context_id = :CID",
array(":UID" => $pieces[0], ":CID" => $pieces[2])
);
if ( $row === false ) {
if ( $minted === null ) {
return 'Metadata failed';
}
$row = $minted;
}

return array($row, $png, $pieces, $badge);
return \Tsugi\Core\Badges::parseAssertionId($encrypted, $lesson);
}

function get_assertion($encrypted, $date, $code, $badge, $title, $email ) {
global $CFG;

$config_error = check_badge_config();
$public = is_object($badge) && isset($badge->image) && \Tsugi\Services\Badges\CourseBadges::isPublicImage($badge->image);
$config_error = \Tsugi\Core\Badges::checkBadgeConfig( ! $public );
if ( $config_error !== false ) {
return $config_error;
}

$image = $CFG->badge_url.'/'.$code.'.png';
$image = \Tsugi\Core\Badges::liveImageUrl($badge, $code, \Tsugi\Core\Badges::contextIdFromAssertion($encrypted));
$recepient = 'sha256$' . hash('sha256', $email . $CFG->badge_assert_salt);
$assert_id = $CFG->wwwroot . "/badges/assert.php?id=". $encrypted;
$badge_url = $CFG->wwwroot . "/badges/badge-info.php?id=". $encrypted;
Expand Down Expand Up @@ -165,12 +102,13 @@ function get_assertion($encrypted, $date, $code, $badge, $title, $email ) {
function get_badge($encrypted, $code, $badge, $title) {
global $CFG;

$config_error = check_badge_config();
$public = is_object($badge) && isset($badge->image) && \Tsugi\Services\Badges\CourseBadges::isPublicImage($badge->image);
$config_error = \Tsugi\Core\Badges::checkBadgeConfig( ! $public );
if ( $config_error !== false ) {
return $config_error;
}

$image = $CFG->badge_url.'/'.$code.'.png';
$image = \Tsugi\Core\Badges::liveImageUrl($badge, $code, \Tsugi\Core\Badges::contextIdFromAssertion($encrypted));
// Use code-based URL instead of encrypted ID
$badge_url = $CFG->wwwroot . "/badges/badge-info.php?code=". urlencode($code);
$badge_issuer = $CFG->wwwroot . "/badges/badge-issuer.php";
Expand All @@ -193,7 +131,8 @@ function get_badge($encrypted, $code, $badge, $title) {
function get_issuer($encrypted, $code, $badge, $title) {
global $CFG;

$config_error = check_badge_config();
$public = is_object($badge) && isset($badge->image) && \Tsugi\Services\Badges\CourseBadges::isPublicImage($badge->image);
$config_error = \Tsugi\Core\Badges::checkBadgeConfig( ! $public );
if ( $config_error !== false ) {
return $config_error;
}
Expand Down
7 changes: 5 additions & 2 deletions badges/baker.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@
require_once "../config.php";
require_once "badge-util.php";

$l = Manifest::requireCurrentLessons();

$PDOX = LTIX::getConnection();

$url = $_SERVER['REQUEST_URI'];
$pieces = explode('/',$url);
$encrypted = basename($pieces[count($pieces)-1],'.png');

$l = \Tsugi\Services\Badges\BadgeService::lessonsForAssertion($encrypted);
if ( ! $l ) {
die_with_error_log('Bad badge image');
}

$x = parse_badge_id($encrypted, $l);
if ( is_string($x) ) {
if ( strpos($x, '<h2>Badge Configuration Required</h2>') !== false ) {
Expand Down
Loading
Loading