Example Google Apps Script powered Mozilla Open Badges Issuer Gadget snippet
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2015 Martin Hawksey. All Rights Reserved. | |
// | |
// Licensed under the Apache License, Version 2.0 (the "License"); | |
// you may not use this file except in compliance with the License. | |
// You may obtain a copy of the License at | |
// | |
// http://www.apache.org/licenses/LICENSE-2.0 | |
// | |
// Unless required by applicable law or agreed to in writing, software | |
// distributed under the License is distributed on an "AS IS" BASIS, | |
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
// See the License for the specific language governing permissions and | |
// limitations under the License. | |
// version 0.5 spec assertion template | |
// Need to change bits of this or some code to read from elsewhere | |
var badgeTemplates ={ | |
"assertion":{ | |
"salt":"MASHeHawk53yBl0g", | |
"badge":{ | |
"version":"0.1.0", | |
"name":"ALT Annual Survey 2014 Participant", | |
"image":"https://googledrive.com/host/0B1q6_NgDnxLWN3pVcGxlczc2LW8/ALT-Survey-Badge-2014.png", | |
"description":"Awarded to individuals completing ALT Annual Survey. Add your contribution http://go.alt.ac.uk/ALT-Survey-2014", | |
"criteria":"https://docs.google.com/forms/d/18lPOp_jEj7YZFKaY8Pd1KVdAyWCOt3xnpBW0ZrW_Wz8/viewform", | |
"issuer":{ | |
"origin":"https://www.alt.ac.uk", | |
"name":"Martin Hawksey", | |
"org":"ALT", | |
"contact":"no-reply@alt.ac.uk" | |
} | |
} | |
} | |
}; | |
/* | |
version 1.0 spec assertion template | |
var badgeTemplates = { | |
'assertion':{ | |
'recipient':{ | |
'type':'email', | |
'hashed':true, | |
'salt':'MASHeHawk53yBl0g', | |
// random string to hash | |
}, | |
'image':'https://googledrive.com/host/0B1q6_NgDnxLWN3pVcGxlczc2LW8/ALT-Survey-Badge-2014.png', | |
'badge':ScriptApp.getService().getUrl()+'?type=badge', | |
'verify':{ | |
'type':'hosted', | |
} | |
}, | |
'badge':{ | |
'name':'ALT Annual Survey 2014 Participant', | |
'description':'Awarded to individuals completing ALT Annual Survey. Add your contribution http://go.alt.ac.uk/ALT-Survey-2014', | |
'image':'https://googledrive.com/host/0B1q6_NgDnxLWN3pVcGxlczc2LW8/ALT-Survey-Badge-2014.png', | |
'criteria':'https://docs.google.com/forms/d/18lPOp_jEj7YZFKaY8Pd1KVdAyWCOt3xnpBW0ZrW_Wz8/viewform', | |
'issuer':ScriptApp.getService().getUrl()+'?type=issuer' | |
}, | |
'issuer':{ | |
'name':'Association for Learning Technology', | |
'url':'http://www.alt.ac.uk/', | |
} | |
}; | |
*/ | |
/** | |
* Loads interface. | |
* | |
* @param {Object} e to include | |
* @return {HtmlService} | |
*/ | |
function doGet(e) { | |
var uid = e.parameter.uid; | |
var key = e.parameter.key; | |
var type = e.parameter.type; | |
// if handling a 'type' return the build JSON | |
// this would also work with a version 1.0 spec badgeTemplate | |
if (type){ | |
if (!uid && !key) { | |
var json = JSON.stringify(badgeTemplates[type]); | |
} else { | |
var json = buildAssertion(uid, key); | |
} | |
return ContentService.createTextOutput(json) | |
.setMimeType(ContentService.MimeType.JSON); | |
} | |
// if no type return a human interface | |
if (!uid && !key){ | |
var html = HtmlService.createTemplate('<link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons.css">' + | |
'<?!= include("style"); ?><div id="wrap"><p align="center">Oops: Something is wrong with the link you tried</p></div>'); | |
} else { | |
var html = HtmlService.createTemplateFromFile('ui'); // to use iframe version use 'ui_frame' | |
html.badgeUrl = ScriptApp.getService().getUrl()+'?type=assertion&uid='+uid+'&key='+key; | |
} | |
return html.evaluate() | |
.setTitle('Send Awarded Badges to Mozilla Backpack') | |
.setSandboxMode(HtmlService.SandboxMode.NATIVE); // to use iframe version use HtmlService.SandboxMode.IFRAME | |
} | |
/** | |
* Build an Assertion JSON object. | |
* | |
* @params {string} uid to build assertion | |
* @return {string} a persons badge asserstion | |
*/ | |
function buildAssertion(uid, key){ | |
// open the sheet/form where the badge was issued | |
var doc = SpreadsheetApp.openById(key); | |
// get the sheet named 'IssuedBadges' | |
var sheet = doc.getSheetByName('IssuedBadges'); | |
var maxRows = sheet.getLastRow(); | |
// get first 3 columns (assuming matching [[UUID, Email, Timestamp]] schema | |
var data = sheet.getRange(2, 1, maxRows, 3).getValues(); | |
var i = 0; | |
// work down the UUID column until a match | |
while (i < maxRows) { | |
if (data[i][0] == uid) { | |
break; | |
} | |
i += 1; | |
} | |
// collect corresponding Email and Timestamp | |
var email = data[i][1]; | |
var timestamp = data[i][2]; | |
// building our 0.5 spec assertion | |
badgeTemplates.assertion.issuedOn = Math.round(timestamp.getTime()/1000.0); | |
badgeTemplates.assertion.recipient = hashString(email, badgeTemplates.assertion.salt); | |
/* | |
bits for a version 1.0 spec badgeTemplate | |
badgeTemplates.assertion.uid = uid; | |
badgeTemplates.assertion.recipient.identity = hashString(email, badgeTemplates.assertion.recipient.salt); | |
badgeTemplates.assertion.verify.url = ScriptApp.getService().getUrl()+'?type=assertion&uid='+uid+'&key='+key; | |
*/ | |
return JSON.stringify(badgeTemplates.assertion); | |
} | |
/** | |
* Hashe string with salt | |
* Based on https://github.com/mozilla/openbadges/wiki/How-to-hash-&-salt-in-various-languages. | |
* | |
* @param {string} text to hash | |
* @param {string} salt to hash email with | |
* @return {string} of salt and hash email | |
*/ | |
function hashString(text, salt) { | |
var hash = CryptoJS.SHA256(text+salt); | |
return 'sha256$'+ hash; | |
} | |
/** | |
* Include file in ui. | |
* @param {string} filename to include | |
* @return {string} html | |
*/ | |
function include(filename) { | |
return HtmlService.createHtmlOutputFromFile(filename) | |
.getContent(); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
CryptoJS v3.0.2 | |
code.google.com/p/crypto-js | |
(c) 2009-2012 by Jeff Mott. All rights reserved. | |
code.google.com/p/crypto-js/wiki/License | |
http://crypto-js.googlecode.com/svn/tags/3.0.2/build/rollups/sha256.js | |
*/ | |
// No Edits Necessary | |
var CryptoJS=CryptoJS||function(i,p){var f={},q=f.lib={},j=q.Base=function(){function a(){}return{extend:function(h){a.prototype=this;var d=new a;h&&d.mixIn(h);d.$super=this;return d},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var d in a)a.hasOwnProperty(d)&&(this[d]=a[d]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.$super.extend(this)}}}(),k=q.WordArray=j.extend({init:function(a,h){a= | |
this.words=a||[];this.sigBytes=h!=p?h:4*a.length},toString:function(a){return(a||m).stringify(this)},concat:function(a){var h=this.words,d=a.words,c=this.sigBytes,a=a.sigBytes;this.clamp();if(c%4)for(var b=0;b<a;b++)h[c+b>>>2]|=(d[b>>>2]>>>24-8*(b%4)&255)<<24-8*((c+b)%4);else if(65535<d.length)for(b=0;b<a;b+=4)h[c+b>>>2]=d[b>>>2];else h.push.apply(h,d);this.sigBytes+=a;return this},clamp:function(){var a=this.words,b=this.sigBytes;a[b>>>2]&=4294967295<<32-8*(b%4);a.length=i.ceil(b/4)},clone:function(){var a= | |
j.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var b=[],d=0;d<a;d+=4)b.push(4294967296*i.random()|0);return k.create(b,a)}}),r=f.enc={},m=r.Hex={stringify:function(a){for(var b=a.words,a=a.sigBytes,d=[],c=0;c<a;c++){var e=b[c>>>2]>>>24-8*(c%4)&255;d.push((e>>>4).toString(16));d.push((e&15).toString(16))}return d.join("")},parse:function(a){for(var b=a.length,d=[],c=0;c<b;c+=2)d[c>>>3]|=parseInt(a.substr(c,2),16)<<24-4*(c%8);return k.create(d,b/2)}},s=r.Latin1={stringify:function(a){for(var b= | |
a.words,a=a.sigBytes,d=[],c=0;c<a;c++)d.push(String.fromCharCode(b[c>>>2]>>>24-8*(c%4)&255));return d.join("")},parse:function(a){for(var b=a.length,d=[],c=0;c<b;c++)d[c>>>2]|=(a.charCodeAt(c)&255)<<24-8*(c%4);return k.create(d,b)}},g=r.Utf8={stringify:function(a){try{return decodeURIComponent(escape(s.stringify(a)))}catch(b){throw Error("Malformed UTF-8 data");}},parse:function(a){return s.parse(unescape(encodeURIComponent(a)))}},b=q.BufferedBlockAlgorithm=j.extend({reset:function(){this._data=k.create(); | |
this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=g.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var b=this._data,d=b.words,c=b.sigBytes,e=this.blockSize,f=c/(4*e),f=a?i.ceil(f):i.max((f|0)-this._minBufferSize,0),a=f*e,c=i.min(4*a,c);if(a){for(var g=0;g<a;g+=e)this._doProcessBlock(d,g);g=d.splice(0,a);b.sigBytes-=c}return k.create(g,c)},clone:function(){var a=j.clone.call(this);a._data=this._data.clone();return a},_minBufferSize:0});q.Hasher=b.extend({init:function(){this.reset()}, | |
reset:function(){b.reset.call(this);this._doReset()},update:function(a){this._append(a);this._process();return this},finalize:function(a){a&&this._append(a);this._doFinalize();return this._hash},clone:function(){var a=b.clone.call(this);a._hash=this._hash.clone();return a},blockSize:16,_createHelper:function(a){return function(b,d){return a.create(d).finalize(b)}},_createHmacHelper:function(a){return function(b,d){return e.HMAC.create(a,d).finalize(b)}}});var e=f.algo={};return f}(Math); | |
(function(i){var p=CryptoJS,f=p.lib,q=f.WordArray,f=f.Hasher,j=p.algo,k=[],r=[];(function(){function f(a){for(var b=i.sqrt(a),d=2;d<=b;d++)if(!(a%d))return!1;return!0}function g(a){return 4294967296*(a-(a|0))|0}for(var b=2,e=0;64>e;)f(b)&&(8>e&&(k[e]=g(i.pow(b,0.5))),r[e]=g(i.pow(b,1/3)),e++),b++})();var m=[],j=j.SHA256=f.extend({_doReset:function(){this._hash=q.create(k.slice(0))},_doProcessBlock:function(f,g){for(var b=this._hash.words,e=b[0],a=b[1],h=b[2],d=b[3],c=b[4],i=b[5],j=b[6],k=b[7],l=0;64> | |
l;l++){if(16>l)m[l]=f[g+l]|0;else{var n=m[l-15],o=m[l-2];m[l]=((n<<25|n>>>7)^(n<<14|n>>>18)^n>>>3)+m[l-7]+((o<<15|o>>>17)^(o<<13|o>>>19)^o>>>10)+m[l-16]}n=k+((c<<26|c>>>6)^(c<<21|c>>>11)^(c<<7|c>>>25))+(c&i^~c&j)+r[l]+m[l];o=((e<<30|e>>>2)^(e<<19|e>>>13)^(e<<10|e>>>22))+(e&a^e&h^a&h);k=j;j=i;i=c;c=d+n|0;d=h;h=a;a=e;e=n+o|0}b[0]=b[0]+e|0;b[1]=b[1]+a|0;b[2]=b[2]+h|0;b[3]=b[3]+d|0;b[4]=b[4]+c|0;b[5]=b[5]+i|0;b[6]=b[6]+j|0;b[7]=b[7]+k|0},_doFinalize:function(){var f=this._data,g=f.words,b=8*this._nDataBytes, | |
e=8*f.sigBytes;g[e>>>5]|=128<<24-e%32;g[(e+64>>>9<<4)+15]=b;f.sigBytes=4*g.length;this._process()}});p.SHA256=f._createHelper(j);p.HmacSHA256=f._createHmacHelper(j)})(Math); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<style> | |
#wrap { | |
padding: 30px; | |
max-width: 380px; | |
position: relative; | |
margin: 0 auto; | |
margin-top: 40px; | |
box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); | |
} | |
.start-twitter-auth { | |
text-align:center; | |
} | |
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons.css"> | |
<?!= include('style'); ?> | |
<div id="wrap"> | |
<!-- START Customise your instructions as you like --> | |
<h1>Send Awarded Badges to Mozilla Backpack</h1> | |
<div class="info"><p>Congratulations on getting your badges! You can now send them to your Mozilla Backback</p></div> | |
<!-- ENDOF Customise your instructions as you like --> | |
<!-- START **REQUIRED** --> | |
<div id="badges_form"> | |
<div id="loading">Loading...</div> | |
</div> | |
<!-- END **REQUIRED** --> | |
</div> | |
<input type="hidden" id="param" value="<?= badgeUrl ?>"> | |
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script> | |
<script> | |
$(function() { | |
// from https://backpack.openbadges.org/issuer.js | |
var root = 'https://backpack.openbadges.org/'; // current api url was this.getRoot(); | |
var url = root + "issuer/frameless?" + (new Date().getTime()); | |
// make a form of badge assertions to POST to backpack | |
var form = $('<form method="POST"></form>').attr('action', url).appendTo($('#badges_form')); | |
$('<input type="hidden" name="assertions">').val(<?= badgeUrl ?>).appendTo(form); // switch the type to text if you need to debug | |
form.removeAttr('onsubmit'); // hack http://stackoverflow.com/a/19467112/1027723 | |
// button to kick off process | |
$('<input type="submit" value="Send to Mozilla Backpack" class="action"/>').appendTo(form); | |
$('#loading').hide(); | |
}); | |
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script> | |
<script src="https://backpack.openbadges.org/issuer.js"></script> | |
<script> | |
$(function() { | |
OpenBadges.issue(['<?= badgeUrl; ?>'], function(errors, successes) { | |
//... | |
}); | |
}); | |
</script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment