From 5ce8a6ecc75958c55e3a8099a0a69cd8a5e43017 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Loguercio?=
Date: Fri, 18 Dec 2015 19:29:44 -0500
Subject: [PATCH] Badge Baker Offline Feature - Refs #7978
---
main/badge/issued.php | 36 ++++++
main/inc/lib/baker.lib.php | 165 +++++++++++++++++++++++++
main/template/default/skill/issued.tpl | 11 +-
3 files changed, 211 insertions(+), 1 deletion(-)
create mode 100644 main/inc/lib/baker.lib.php
diff --git a/main/badge/issued.php b/main/badge/issued.php
index bc371ab2bd..abd32048ae 100644
--- a/main/badge/issued.php
+++ b/main/badge/issued.php
@@ -6,6 +6,7 @@
* @package chamilo.badge
*/
require_once '../inc/global.inc.php';
+require_once '../inc/lib/baker.lib.php';
$userId = isset($_GET['user']) ? intval($_GET['user']) : 0;
$skillId = isset($_GET['skill']) ? intval($_GET['skill']) : 0;
@@ -101,11 +102,46 @@ if ($allowExport) {
$htmlHeadXtra[] = '';
}
+$objSkill = new Skill();
+$skills = $objSkill->get($skillId);
+$unbakedBadge = api_get_path(SYS_UPLOAD_PATH) . "badges/".$skills['icon'];
+
+$unbakedBadge = file_get_contents($unbakedBadge);
+$badgeInfoError = false;
+$personalBadge = "";
+$png = new PNGImageBaker($unbakedBadge);
+
+if ($png->checkChunks("tEXt", "openbadges")) {
+ $bakedInfo = $png->addChunk("tEXt", "openbadges", $assertionUrl);
+ $bakedBadge = UserManager::getUserPathById($userId, "system");
+ $bakedBadge = $bakedBadge.'badges';
+ if (!file_exists($bakedBadge)) {
+ mkdir($bakedBadge, api_get_permissions_for_new_directories(), true);
+ }
+ $skillRelUserId = $userSkills[0]->getId();
+ if (!file_exists($bakedBadge . "/badge_" . $skillRelUserId)) {
+ file_put_contents($bakedBadge . "/badge_" . $skillRelUserId . ".png", $bakedInfo);
+ }
+
+ //Process to validate a baked badge
+ $badgeContent = file_get_contents($bakedBadge . "/badge_" . $skillRelUserId . ".png");
+ $verifyBakedBadge = $png->extractBadgeInfo($badgeContent);
+ if (!is_array($verifyBakedBadge)) {
+ $badgeInfoError = true;
+ }
+
+ if (!$badgeInfoError) {
+ $personalBadge = UserManager::getUserPathById($userId, "web");
+ $personalBadge = $personalBadge."badges/badge_" . $skillRelUserId . ".png";
+ }
+}
$template = new Template('');
$template->assign('skill_info', $skillInfo);
$template->assign('user_info', $userInfo);
$template->assign('allow_export', $allowExport);
+$template->assign('badge_error', $badgeInfoError);
+$template->assign('personal_badge', $personalBadge);
if ($allowExport) {
$template->assign('assertions', $badgeAssertions);
diff --git a/main/inc/lib/baker.lib.php b/main/inc/lib/baker.lib.php
new file mode 100644
index 0000000000..920f8d32cd
--- /dev/null
+++ b/main/inc/lib/baker.lib.php
@@ -0,0 +1,165 @@
+_contents = $contents;
+ $png_signature = pack("C8", 137, 80, 78, 71, 13, 10, 26, 10);
+ // Read 8 bytes of PNG header and verify.
+ $header = substr($this->_contents, 0, 8);
+ if ($header != $png_signature) {
+ echo 'This is not a valid PNG image';
+ }
+ $this->_size = strlen($this->_contents);
+ $this->_chunks = array();
+ // Skip 8 bytes of IHDR image header.
+ $position = 8;
+ do {
+ $chunk = @unpack('Nsize/a4type', substr($this->_contents, $position, 8));
+ $this->_chunks[$chunk['type']][] = substr($this->_contents, $position + 8, $chunk['size']);
+ // Skip 12 bytes chunk overhead.
+ $position += $chunk['size'] + 12;
+ } while ($position < $this->_size);
+ }
+
+ /**
+ * Checks if a key already exists in the chunk of said type.
+ * We need to avoid writing same keyword into file chunks.
+ *
+ * @param string $type Chunk type, like iTXt, tEXt, etc.
+ * @param string $check Keyword that needs to be checked.
+ *
+ * @return boolean (true|false) True if file is safe to write this keyword, false otherwise.
+ */
+ public function checkChunks($type, $check) {
+ if (array_key_exists($type, $this->_chunks)) {
+ foreach (array_keys($this->_chunks[$type]) as $typekey) {
+ list($key, $data) = explode("\0", $this->_chunks[$type][$typekey]);
+ if (strcmp($key, $check) == 0) {
+ echo 'Key "' . $check . '" already exists in "' . $type . '" chunk.';
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Add a chunk by type with given key and text
+ *
+ * @param string $chunkType Chunk type, like iTXt, tEXt, etc.
+ * @param string $key Keyword that needs to be added.
+ * @param string $value Currently an assertion URL that is added to an image metadata.
+ *
+ * @return string $result File content with a new chunk as a string.
+ */
+ public function addChunk($chunkType, $key, $value) {
+
+ $chunkData = $key . "\0" . $value;
+ $crc = pack("N", crc32($chunkType . $chunkData));
+ $len = pack("N", strlen($chunkData));
+
+ $newChunk = $len . $chunkType . $chunkData . $crc;
+ $result = substr($this->_contents, 0, $this->_size - 12)
+ . $newChunk
+ . substr($this->_contents, $this->_size - 12, 12);
+ return $result;
+ }
+
+ /**
+ * removes a chunk by type with given key and text
+ *
+ * @param string $chunkType Chunk type, like iTXt, tEXt, etc.
+ * @param string $key Keyword that needs to be deleted.
+ * @param string $png the png image.
+ *
+ * @return string $result New File content.
+ */
+ public function removeChunks($chunkType, $key, $png) {
+ // Read the magic bytes and verify
+ $retval = substr($png,0,8);
+ $ipos = 8;
+ if ($retval != "\x89PNG\x0d\x0a\x1a\x0a")
+ throw new Exception('Is not a valid PNG image');
+ // Loop through the chunks. Byte 0-3 is length, Byte 4-7 is type
+ $chunkHeader = substr($png,$ipos,8);
+ $ipos = $ipos + 8;
+ while ($chunkHeader) {
+ // Extract length and type from binary data
+ $chunk = @unpack('Nsize/a4type', $chunkHeader);
+ $skip = false;
+ if ( $chunk['type'] == $chunkType ) {
+ $data = substr($png,$ipos,$chunk['size']);
+ $sections = explode("\0", $data);
+ print_r($sections);
+ if ( $sections[0] == $key ) $skip = true;
+ }
+ // Extract the data and the CRC
+ $data = substr($png,$ipos,$chunk['size']+4);
+ $ipos = $ipos + $chunk['size'] + 4;
+ // Add in the header, data, and CRC
+ if ( ! $skip ) $retval = $retval . $chunkHeader . $data;
+ // Read next chunk header
+ $chunkHeader = substr($png,$ipos,8);
+ $ipos = $ipos + 8;
+ }
+ return $retval;
+ }
+
+ /**
+ * Extracts the baked PNG info by the Key
+ *
+ * @param string $png the png image
+ * @param string $key Keyword that needs to be searched.
+ *
+ * @return mixed - If there is an error - boolean false is returned
+ * If there is PNG information that matches the key an array is returned
+ *
+ */
+ public function extractBadgeInfo($png, $key='openbadges') {
+ // Read the magic bytes and verify
+ $retval = substr($png,0,8);
+ $ipos = 8;
+ if ($retval != "\x89PNG\x0d\x0a\x1a\x0a") {
+ return false;
+ }
+
+ // Loop through the chunks. Byte 0-3 is length, Byte 4-7 is type
+ $chunkHeader = substr($png,$ipos,8);
+ $ipos = $ipos + 8;
+ while ($chunkHeader) {
+ // Extract length and type from binary data
+ $chunk = @unpack('Nsize/a4type', $chunkHeader);
+ $skip = false;
+ if ($chunk['type'] == 'tEXt') {
+ $data = substr($png,$ipos,$chunk['size']);
+ $sections = explode("\0", $data);
+ if ($sections[0] == $key) {
+ return $sections;
+ }
+ }
+ // Extract the data and the CRC
+ $data = substr($png,$ipos,$chunk['size']+4);
+ $ipos = $ipos + $chunk['size'] + 4;
+
+ // Read next chunk header
+ $chunkHeader = substr($png,$ipos,8);
+ $ipos = $ipos + 8;
+ }
+ }
+}
\ No newline at end of file
diff --git a/main/template/default/skill/issued.tpl b/main/template/default/skill/issued.tpl
index b201c3804b..334085f546 100644
--- a/main/template/default/skill/issued.tpl
+++ b/main/template/default/skill/issued.tpl
@@ -19,7 +19,16 @@
{% endfor %}
-
+
+ {% if badge_error %}
+ {{ 'BakedBadgeProblem'|get_lang }}
+ {% else %}
+
+
+ {{ 'DownloadBadge'|get_lang }}
+
+
+ {% endif %}
{% if allow_export %}