Skip to content

Instantly share code, notes, and snippets.

@mhawksey
Last active August 29, 2015 14:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mhawksey/a547df27a2b3ade42731 to your computer and use it in GitHub Desktop.
Save mhawksey/a547df27a2b3ade42731 to your computer and use it in GitHub Desktop.
Example Google Apps Script powered Mozilla Open Badges Issuer Gadget snippet
// 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();
}
/*
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);
<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>
<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>
<!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