From 270d032a8746f8836d93f6463a27e32c339efbc7 Mon Sep 17 00:00:00 2001
From: jfederico <jesus@123it.ca>
Date: Mon, 11 Dec 2017 15:47:38 +0000
Subject: [PATCH] bbb-lti: Rework on the way XML is parsed in order to fix
 issue with getRecording

---
 bbb-lti/.asscache                             | Bin 154724 -> 0 bytes
 bbb-lti/.gitignore                            |   1 +
 bbb-lti/application.properties                |   3 +-
 bbb-lti/grails-app/conf/BuildConfig.groovy    |   5 +-
 bbb-lti/grails-app/conf/Config.groovy         |   7 +-
 .../org/bigbluebutton/ToolController.groovy   | 304 ++++++++----------
 bbb-lti/grails-app/i18n/messages.properties   |   5 +-
 .../grails-app/i18n/messages_es.properties    |   3 +-
 .../grails-app/i18n/messages_fr.properties    |  85 ++---
 .../bigbluebutton/BigbluebuttonService.groovy | 249 +++++++-------
 .../org/bigbluebutton/LtiService.groovy       |  26 +-
 bbb-lti/grails-app/views/tool/error.gsp       |  68 ++--
 bbb-lti/grails-app/views/tool/index.gsp       | 156 ++++-----
 13 files changed, 438 insertions(+), 474 deletions(-)
 delete mode 100644 bbb-lti/.asscache
 create mode 100644 bbb-lti/.gitignore

diff --git a/bbb-lti/.asscache b/bbb-lti/.asscache
deleted file mode 100644
index be6deceb5e9795140859e536f78de8fe794cf1ef..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 154724
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
Q_~)0O&+uRT?-Tv~A4mMBHvj+t

diff --git a/bbb-lti/.gitignore b/bbb-lti/.gitignore
new file mode 100644
index 0000000000..b1bf7c47d2
--- /dev/null
+++ b/bbb-lti/.gitignore
@@ -0,0 +1 @@
+.asscache
\ No newline at end of file
diff --git a/bbb-lti/application.properties b/bbb-lti/application.properties
index 68fe1dc6a3..a00806dd9e 100644
--- a/bbb-lti/application.properties
+++ b/bbb-lti/application.properties
@@ -2,4 +2,5 @@
 #Fri Aug 19 19:12:11 UTC 2016
 app.grails.version=2.5.2
 app.name=lti
-app.version=0.3
+app.servlet.version=3.0
+app.version=0.4
diff --git a/bbb-lti/grails-app/conf/BuildConfig.groovy b/bbb-lti/grails-app/conf/BuildConfig.groovy
index 0bf647fec3..4a84c2785a 100644
--- a/bbb-lti/grails-app/conf/BuildConfig.groovy
+++ b/bbb-lti/grails-app/conf/BuildConfig.groovy
@@ -1,4 +1,4 @@
-/* 
+/*
     BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
 
     Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
@@ -14,7 +14,7 @@
 
     You should have received a copy of the GNU Lesser General Public License along
     with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
-*/    
+*/
 
 grails.servlet.version = "3.0" // Change depending on target container compliance (2.5 or 3.0)
 grails.project.class.dir = "target/classes"
@@ -65,6 +65,7 @@ grails.project.dependency.resolution = {
     }
 
     dependencies {
+        compile 'org.json:json:20171018'
         // specify dependencies here under either 'build', 'compile', 'runtime', 'test' or 'provided' scopes e.g.
         // runtime 'mysql:mysql-connector-java:5.1.29'
         // runtime 'org.postgresql:postgresql:9.3-1101-jdbc41'
diff --git a/bbb-lti/grails-app/conf/Config.groovy b/bbb-lti/grails-app/conf/Config.groovy
index 40ca78588a..966ea2329e 100644
--- a/bbb-lti/grails-app/conf/Config.groovy
+++ b/bbb-lti/grails-app/conf/Config.groovy
@@ -1,4 +1,4 @@
-/* 
+/*
     BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
 
     Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
@@ -14,7 +14,7 @@
 
     You should have received a copy of the GNU Lesser General Public License along
     with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
-*/    
+*/
 
 // locations to search for config files that get merged into the main config;
 // config files can be ConfigSlurper scripts, Java properties files, or classes
@@ -103,6 +103,9 @@ grails.hibernate.pass.readonly = false
 // configure passing read-only to OSIV session by default, requires "singleSession = false" OSIV mode
 grails.hibernate.osiv.readonly = false
 
+// Enable hot reloading for production environments
+grails.gsp.enable.reload=true
+
 environments {
     development {
         grails.logging.jul.usebridge = true
diff --git a/bbb-lti/grails-app/controllers/org/bigbluebutton/ToolController.groovy b/bbb-lti/grails-app/controllers/org/bigbluebutton/ToolController.groovy
index 600218fedf..b868a2b564 100644
--- a/bbb-lti/grails-app/controllers/org/bigbluebutton/ToolController.groovy
+++ b/bbb-lti/grails-app/controllers/org/bigbluebutton/ToolController.groovy
@@ -48,125 +48,103 @@ class ToolController {
 
     def index = {
         log.debug CONTROLLER_NAME + "#index"
-        if( ltiService.consumerMap == null) ltiService.initConsumerMap()
-
+        if (ltiService.consumerMap == null) {
+            ltiService.initConsumerMap()
+        }
         setLocalization(params)
-
         params.put(REQUEST_METHOD, request.getMethod().toUpperCase())
         ltiService.logParameters(params)
-
-        if( request.post ){
-            def scheme = request.isSecure()? "https": "http"
-            def endPoint = scheme + "://" + ltiService.endPoint + "/" + grailsApplication.metadata['app.name'] + "/" + params.get("controller") + (params.get("format") != null? "." + params.get("format"): "")
-            log.info "endPoint: " + endPoint
-            Map<String, String> result = new HashMap<String, String>()
-            ArrayList<String> missingParams = new ArrayList<String>()
-
-            if (hasAllRequiredParams(params, missingParams)) {
-                def sanitizedParams = sanitizePrametersForBaseString(params)
-                def consumer = ltiService.getConsumer(params.get(Parameter.CONSUMER_ID))
-                if ( !ltiService.hasRestrictedAccess() || consumer != null) {
-                    if (ltiService.hasRestrictedAccess() ) {
-                        log.debug "Found consumer with key " + consumer.get("key") //+ " and sharedSecret " + consumer.get("secret")
-                    }
-
-                    if (!ltiService.hasRestrictedAccess() || checkValidSignature(params.get(REQUEST_METHOD), endPoint, consumer.get("secret"), sanitizedParams, params.get(Parameter.OAUTH_SIGNATURE))) {
-                        if (!ltiService.hasRestrictedAccess() ) {
-                            log.debug  "Access not restricted, valid signature is not required."
-                        } else {
-                            log.debug  "The message has a valid signature."
-                        }
-
-                        def mode = params.containsKey(Parameter.CUSTOM_MODE)? params.get(Parameter.CUSTOM_MODE): ltiService.mode
-                        if( !"extended".equals(mode) ) {
-                            log.debug  "LTI service running in simple mode."
-                            result = doJoinMeeting(params)
-                        } else {
-                            log.debug  "LTI service running in extended mode."
-                            if ( !Boolean.parseBoolean(params.get(Parameter.CUSTOM_RECORD)) && !ltiService.allRecordedByDefault() ) {
-                                log.debug  "Parameter custom_record was not sent; immediately redirecting to BBB session!"
-                                result = doJoinMeeting(params)
-                            }
-                        }
-
-                    } else {
-                        log.debug  "The message has NOT a valid signature."
-                        result.put("resultMessageKey", "InvalidSignature")
-                        result.put("resultMessage", "Invalid signature (" + params.get(Parameter.OAUTH_SIGNATURE) + ").")
-                    }
-
-                } else {
-                    result.put("resultMessageKey", "ConsumerNotFound")
-                    result.put("resultMessage", "Consumer with id = " + params.get(Parameter.CONSUMER_ID) + " was not found.")
-                }
-
-            } else {
-                String missingStr = ""
-                for(String str:missingParams) {
-                    missingStr += str + ", ";
-                }
-                result.put("resultMessageKey", "MissingRequiredParameter")
-                result.put("resultMessage", "Missing parameters [$missingStr]")
+        // On get requests render the common cartridge.
+        if (request.get) {
+            render(text: getCartridgeXML(), contentType: "text/xml", encoding: "UTF-8")
+            return
+        }
+        // On post request proceed with the launch.
+        def endPoint = ltiService.getScheme(request) + "://" + ltiService.endPoint + "/" + grailsApplication.metadata['app.name'] + "/" + params.get("controller") + (params.get("format") != null ? "." + params.get("format") : "")
+        log.info "endPoint: " + endPoint
+        ArrayList<String> missingParams = new ArrayList<String>()
+
+        if (!hasAllRequiredParams(params, missingParams)) {
+            String missingStr = ""
+            for (String str:missingParams) {
+                missingStr += str + ", ";
             }
+            return renderError("MissingRequiredParameter", "Missing parameters [$missingStr]")
+        }
 
-            if( result.containsKey("resultMessageKey") ) {
-                log.debug "Error [resultMessageKey:'" + result.get("resultMessageKey") + "', resultMessage:'" + result.get("resultMessage") + "']"
-                render(view: "error", model: ['resultMessageKey': result.get("resultMessageKey"), 'resultMessage': result.get("resultMessage")])
-
-            } else {
-                session["params"] = params
-                render(view: "index", model: ['params': params, 'recordingList': getSanitizedRecordings(params), 'ismoderator': bigbluebuttonService.isModerator(params)])
+        def sanitizedParams = sanitizePrametersForBaseString(params)
+        def consumer = ltiService.getConsumer(params.get(Parameter.CONSUMER_ID))
+        if (ltiService.hasRestrictedAccess()) {
+            if (consumer == null) {
+                return renderError("ConsumerNotFound", "Consumer with id = " + params.get(Parameter.CONSUMER_ID) + " was not found.")
             }
+            log.debug "Found consumer with key " + consumer.get("key") //+ " and sharedSecret " + consumer.get("secret")
+        }
+        def validSignature = checkValidSignature(params.get(REQUEST_METHOD), endPoint, consumer.get("secret"), sanitizedParams, params.get(Parameter.OAUTH_SIGNATURE))
+        if (ltiService.hasRestrictedAccess()) {
+            if (!validSignature) {
+                log.debug  "The message has NOT a valid signature."
+                return renderError("InvalidSignature", "Invalid signature (" + params.get(Parameter.OAUTH_SIGNATURE) + ").")
+            }
+            log.debug  "The message has a valid signature."
         } else {
-            render(text: getCartridgeXML(), contentType: "text/xml", encoding: "UTF-8")
+            log.debug  "Access not restricted, valid signature is not required."
+        }
+        def mode = params.containsKey(Parameter.CUSTOM_MODE)? params.get(Parameter.CUSTOM_MODE): ltiService.mode
+        if (!"extended".equals(mode)) {
+            log.debug  "LTI service running in simple mode."
+            def result = doJoinMeeting(params)
+            return
         }
+        log.debug  "LTI service running in extended mode."
+        if (!Boolean.parseBoolean(params.get(Parameter.CUSTOM_RECORD)) && !ltiService.allRecordedByDefault()) {
+            log.debug  "Parameter custom_record was not sent; immediately redirecting to BBB session!"
+            def result = doJoinMeeting(params)
+            return
+        }
+        session["params"] = params
+        render(view: "index", model: ['params': params, 'recordingList': getSanitizedRecordings(params), 'ismoderator': bigbluebuttonService.isModerator(params)])
     }
 
     def join = {
         if( ltiService.consumerMap == null) ltiService.initConsumerMap()
         log.debug CONTROLLER_NAME + "#join"
-        Map<String, String> result
-
+        def result
         def sessionParams = session["params"]
-
         if( sessionParams != null ) {
             log.debug "params: " + params
             log.debug "sessionParams: " + sessionParams
             result = doJoinMeeting(sessionParams)
         } else {
             result = new HashMap<String, String>()
-            result.put("resultMessageKey", "InvalidSession")
-            result.put("resultMessage", "Invalid session. User can not execute this action.")
+            result.put("messageKey", "InvalidSession")
+            result.put("message", "Invalid session. User can not execute this action.")
         }
-
-        if( result.containsKey("resultMessageKey")) {
-            log.debug "Error [resultMessageKey:'" + result.get("resultMessageKey") + "', resultMessage:'" + result.get("resultMessage") + "']"
-            render(view: "error", model: ['resultMessageKey': result.get("resultMessageKey"), 'resultMessage': result.get("resultMessage")])
+        if (result != null && result.containsKey("messageKey")) {
+            log.debug "Error [messageKey:'" + result.get("messageKey") + "', message:'" + result.get("message") + "']"
+            render(view: "error", model: ['messageKey': result.get("messageKey"), 'message': result.get("message")])
         }
     }
 
     def publish = {
         log.debug CONTROLLER_NAME + "#publish"
         Map<String, String> result
-
         def sessionParams = session["params"]
-
         if( sessionParams == null ) {
             result = new HashMap<String, String>()
-            result.put("resultMessageKey", "InvalidSession")
-            result.put("resultMessage", "Invalid session. User can not execute this action.")
+            result.put("messageKey", "InvalidSession")
+            result.put("message", "Invalid session. User can not execute this action.")
         } else if ( !bigbluebuttonService.isModerator(sessionParams) ) {
             result = new HashMap<String, String>()
-            result.put("resultMessageKey", "NotAllowed")
-            result.put("resultMessage", "User not allowed to execute this action.")
+            result.put("messageKey", "NotAllowed")
+            result.put("message", "User not allowed to execute this action.")
         } else {
-            //Execute the publish command
+            // Execute the publish command
             result = bigbluebuttonService.doPublishRecordings(params)
         }
-
-        if( result.containsKey("resultMessageKey")) {
-            log.debug "Error [resultMessageKey:'" + result.get("resultMessageKey") + "', resultMessage:'" + result.get("resultMessage") + "']"
-            render(view: "error", model: ['resultMessageKey': result.get("resultMessageKey"), 'resultMessage': result.get("resultMessage")])
+        if( result.containsKey("messageKey")) {
+            log.debug "Error [messageKey:'" + result.get("messageKey") + "', message:'" + result.get("message") + "']"
+            render(view: "error", model: ['messageKey': result.get("messageKey"), 'message': result.get("message")])
         } else {
             render(view: "index", model: ['params': sessionParams, 'recordingList': getSanitizedRecordings(sessionParams), 'ismoderator': bigbluebuttonService.isModerator(sessionParams)])
         }
@@ -175,25 +153,22 @@ class ToolController {
     def delete = {
         log.debug CONTROLLER_NAME + "#delete"
         Map<String, String> result
-
         def sessionParams = session["params"]
-
         if( sessionParams == null ) {
             result = new HashMap<String, String>()
-            result.put("resultMessageKey", "InvalidSession")
-            result.put("resultMessage", "Invalid session. User can not execute this action.")
+            result.put("messageKey", "InvalidSession")
+            result.put("message", "Invalid session. User can not execute this action.")
         } else if ( !bigbluebuttonService.isModerator(sessionParams) ) {
             result = new HashMap<String, String>()
-            result.put("resultMessageKey", "NotAllowed")
-            result.put("resultMessage", "User not allowed to execute this action.")
+            result.put("messageKey", "NotAllowed")
+            result.put("message", "User not allowed to execute this action.")
         } else {
-            //Execute the delete command
+            // Execute the delete command.
             result = bigbluebuttonService.doDeleteRecordings(params)
         }
-
-        if( result.containsKey("resultMessageKey")) {
-            log.debug "Error [resultMessageKey:'" + result.get("resultMessageKey") + "', resultMessage:'" + result.get("resultMessage") + "']"
-            render(view: "error", model: ['resultMessageKey': result.get("resultMessageKey"), 'resultMessage': result.get("resultMessage")])
+        if( result.containsKey("messageKey")) {
+            log.debug "Error [messageKey:'" + result.get("messageKey") + "', message:'" + result.get("message") + "']"
+            render(view: "error", model: ['messageKey': result.get("messageKey"), 'message': result.get("message")])
         } else {
             render(view: "index", model: ['params': sessionParams, 'recordingList': getSanitizedRecordings(sessionParams), 'ismoderator': bigbluebuttonService.isModerator(sessionParams)])
         }
@@ -203,48 +178,39 @@ class ToolController {
         String locale = params.get(Parameter.LAUNCH_LOCALE)
         locale = (locale == null || locale.equals("")?"en":locale)
         String[] localeCodes = locale.split("_")
-        //Localize the default welcome message
-        if( localeCodes.length > 1 )
+        // Localize the default welcome message
+        session['org.springframework.web.servlet.i18n.SessionLocaleResolver.LOCALE'] = new Locale(localeCodes[0])
+        if (localeCodes.length > 1) {
             session['org.springframework.web.servlet.i18n.SessionLocaleResolver.LOCALE'] = new Locale(localeCodes[0], localeCodes[1])
-        else
-            session['org.springframework.web.servlet.i18n.SessionLocaleResolver.LOCALE'] = new Locale(localeCodes[0])
+        }
     }
 
     private Object doJoinMeeting(Map<String, String> params) {
-        Map<String, String> result = new HashMap<String, String>()
-
         setLocalization(params)
         String welcome = message(code: "bigbluebutton.welcome.header", args: ["\"{0}\"", "\"{1}\""]) + "<br>"
-
         // Check for [custom_]welcome parameter being passed from the LTI
-        if ( params.containsKey(Parameter.CUSTOM_WELCOME) && params.get(Parameter.CUSTOM_WELCOME) != null ) {
+        if (params.containsKey(Parameter.CUSTOM_WELCOME) && params.get(Parameter.CUSTOM_WELCOME) != null) {
             welcome = params.get(Parameter.CUSTOM_WELCOME) + "<br>"
             log.debug "Overriding default welcome message with: [" + welcome + "]"
         }
-
-        if ( params.containsKey(Parameter.CUSTOM_RECORD) && Boolean.parseBoolean(params.get(Parameter.CUSTOM_RECORD)) || ltiService.allRecordedByDefault() ) {
+        if (params.containsKey(Parameter.CUSTOM_RECORD) && Boolean.parseBoolean(params.get(Parameter.CUSTOM_RECORD)) || ltiService.allRecordedByDefault()) {
             welcome += "<br><b>" + message(code: "bigbluebutton.welcome.record") + "</b><br>"
             log.debug "Adding record warning to welcome message, welcome is now: [" + welcome + "]"
         }
-
-        if ( params.containsKey(Parameter.CUSTOM_DURATION) && Integer.parseInt(params.get(Parameter.CUSTOM_DURATION)) > 0 ) {
+        if (params.containsKey(Parameter.CUSTOM_DURATION) && Integer.parseInt(params.get(Parameter.CUSTOM_DURATION)) > 0) {
             welcome += "<br><b>" + message(code: "bigbluebutton.welcome.duration", args: [params.get(Parameter.CUSTOM_DURATION)]) + "</b><br>"
             log.debug "Adding duration warning to welcome message, welcome is now: [" + welcome + "]"
         }
-
         welcome += "<br>" + message(code: "bigbluebutton.welcome.footer") + "<br>"
-
         String destinationURL = bigbluebuttonService.getJoinURL(params, welcome, ltiService.mode)
-        log.debug "redirecting to " + destinationURL
-
-        if( destinationURL != null ) {
-            redirect(url:destinationURL)
-        } else {
-            result.put("resultMessageKey", "BigBlueButtonServerError")
-            result.put("resultMessage", "The join could not be completed")
+        if (destinationURL == null) {
+            Map<String, String> result = new HashMap<String, String>()
+            result.put("messageKey", "BigBlueButtonServerError")
+            result.put("message", "The join could not be completed")
+            return result
         }
-
-        return result
+        log.debug "It is redirecting to " + destinationURL
+        redirect(url:destinationURL)
     }
 
     /**
@@ -258,14 +224,15 @@ class ToolController {
             if (key == "action" || key == "controller" || key == "format") {
                 // Ignore as these are the grails controller and action tied to this request.
                 continue
-            } else if (key == "oauth_signature") {
-                // We don't need this as part of the base string
+            }
+            if (key == "oauth_signature") {
+                // We don't need this as part of the base string.
                 continue
-            } else if (key == "request_method") {
-                // As this is was added by the controller, we don't want it as part of the base string
+            }
+            if (key == "request_method") {
+                // As this is was added by the controller, we don't want it as part of the base string.
                 continue
             }
-
             reqProp.setProperty(key, params.get(key));
         }
         return reqProp
@@ -279,24 +246,19 @@ class ToolController {
      */
     private boolean hasAllRequiredParams(Map<String, String> params, ArrayList<String> missingParams) {
         log.debug "Checking for required parameters"
-
-        boolean hasAllParams = true
-        if ( ltiService.hasRestrictedAccess() && !params.containsKey(Parameter.CONSUMER_ID) ) {
+        if (ltiService.hasRestrictedAccess() && !params.containsKey(Parameter.CONSUMER_ID)) {
             missingParams.add(Parameter.CONSUMER_ID);
-            hasAllParams = false;
+            return false
         }
-
-        if ( ltiService.hasRestrictedAccess() && !params.containsKey(Parameter.OAUTH_SIGNATURE)) {
+        if (ltiService.hasRestrictedAccess() && !params.containsKey(Parameter.OAUTH_SIGNATURE)) {
             missingParams.add(Parameter.OAUTH_SIGNATURE);
-            hasAllParams = false;
+            return false
         }
-
-        if ( !params.containsKey(Parameter.RESOURCE_LINK_ID) ) {
+        if (!params.containsKey(Parameter.RESOURCE_LINK_ID)) {
             missingParams.add(Parameter.RESOURCE_LINK_ID);
-            hasAllParams = false;
+            return false
         }
-
-        return hasAllParams
+        return true
     }
 
     /**
@@ -309,32 +271,23 @@ class ToolController {
      * @return - TRUE if the signatures matches the calculated signature
      */
     private boolean checkValidSignature(String method, String url, String conSecret, Properties postProp, String signature) {
-        def validSignature = false
-
-        if ( ltiService.hasRestrictedAccess() ) {
-            try {
-                OAuthMessage oam = new OAuthMessage(method, url, postProp.entrySet())
-                //log.debug "OAuthMessage oam = " + oam.toString()
-
-                HMAC_SHA1 hmac = new HMAC_SHA1()
-                //log.debug "HMAC_SHA1 hmac = " + hmac.toString()
-
-                hmac.setConsumerSecret(conSecret)
-
-                log.debug "Base Message String = [ " + hmac.getBaseString(oam) + " ]\n"
-                String calculatedSignature = hmac.getSignature(hmac.getBaseString(oam))
-                log.debug "Calculated: " + calculatedSignature + " Received: " + signature
-
-                validSignature = calculatedSignature.equals(signature)
-            } catch( Exception e ) {
-                log.debug "Exception error: " + e.message
-            }
-
-        } else {
-            validSignature = true
+        if (!ltiService.hasRestrictedAccess()) {
+            return true;
+        }
+        try {
+            OAuthMessage oam = new OAuthMessage(method, url, postProp.entrySet())
+            //log.debug "OAuthMessage oam = " + oam.toString()
+            HMAC_SHA1 hmac = new HMAC_SHA1()
+            //log.debug "HMAC_SHA1 hmac = " + hmac.toString()
+            hmac.setConsumerSecret(conSecret)
+            log.debug "Base Message String = [ " + hmac.getBaseString(oam) + " ]\n"
+            String calculatedSignature = hmac.getSignature(hmac.getBaseString(oam))
+            log.debug "Calculated: " + calculatedSignature + " Received: " + signature
+            return calculatedSignature.equals(signature)
+        } catch( Exception e ) {
+            log.debug "Exception error: " + e.message
+            return false
         }
-
-        return validSignature
     }
 
     /**
@@ -343,20 +296,34 @@ class ToolController {
      * @return the key:val pairs needed for Basic LTI
      */
     private List<Object> getSanitizedRecordings(Map<String, String> params) {
-        List<Object> recordings = bigbluebuttonService.getRecordings(params)
-        for(Map<String, Object> recording: recordings){
-            /// Calculate duration
+        def recordings = new ArrayList<Object>()
+        def getRecordingsResponse = bigbluebuttonService.getRecordings(params)
+        if (getRecordingsResponse == null) {
+            return recordings
+        }
+        Object response = (Object)getRecordingsResponse.get("recording")
+        if (response instanceof Map<?,?>) {
+            recordings.add(response)
+        }
+        if (response instanceof Collection<?>) {
+            recordings = response
+        }
+        // Sanitize recordings
+        Iterator i = recordings.iterator();
+        while (i.hasNext()) {
+            def recording = i.next()
+            // Calculate duration.
             long endTime = Long.parseLong((String)recording.get("endTime"))
             endTime -= (endTime % 1000)
             long startTime = Long.parseLong((String)recording.get("startTime"))
             startTime -= (startTime % 1000)
             int duration = (endTime - startTime) / 60000
-            /// Add duration
+            // Add duration.
             recording.put("duration", duration )
-            /// Calculate reportDate
+            // Calculate reportDate.
             DateFormat df = new SimpleDateFormat(message(code: "tool.view.dateFormat"))
             String reportDate = df.format(new Date(startTime))
-            /// Add reportDate
+            // Add reportDate.
             recording.put("reportDate", reportDate)
             recording.put("unixDate", startTime / 1000)
         }
@@ -399,4 +366,9 @@ class ToolController {
 
         return cartridge
     }
+
+    private void renderError(key, message) {
+        log.debug "Error [resultMessageKey:'" + key + "', resultMessage:'" + message + "']"
+        render(view: "error", model: ['resultMessageKey': key, 'resultMessage': message])
+    }
 }
diff --git a/bbb-lti/grails-app/i18n/messages.properties b/bbb-lti/grails-app/i18n/messages.properties
index a9a0147405..0385dd5f83 100644
--- a/bbb-lti/grails-app/i18n/messages.properties
+++ b/bbb-lti/grails-app/i18n/messages.properties
@@ -17,7 +17,7 @@
 #
 
 # The welcome.header can be static, however if you want the name of the activity (meeting) to be injected use {0} as part of the text
-# {1} can be used to inject the name of the course 
+# {1} can be used to inject the name of the course
 bigbluebutton.welcome.header=Welcome to <b>{0}</b>!
 bigbluebutton.welcome.footer=To understand how BigBlueButton works see our <a href=\"event:http://www.bigbluebutton.org/content/videos\"><u>tutorial videos</u></a>.<br><br>To join the audio bridge click the headset icon (upper-left hand corner). <b>Please use a headset to avoid causing noise for others.
 bigbluebutton.welcome.record=This meeting is being recorded
@@ -38,9 +38,10 @@ tool.view.recording.unpublish=Unpublish
 tool.view.recording.delete=Delete
 tool.view.activity=Activity
 tool.view.description=Description
+tool.view.preview=Preview
 tool.view.date=Date
 tool.view.duration=Duration
 tool.view.actions=Actions
 tool.view.dateFormat=E, MMM dd, yyyy HH:mm:ss Z
 
-tool.error.general=Connection could not be established.
\ No newline at end of file
+tool.error.general=Connection could not be established.
diff --git a/bbb-lti/grails-app/i18n/messages_es.properties b/bbb-lti/grails-app/i18n/messages_es.properties
index 7e0eedbf33..b749c09d52 100644
--- a/bbb-lti/grails-app/i18n/messages_es.properties
+++ b/bbb-lti/grails-app/i18n/messages_es.properties
@@ -39,9 +39,10 @@ tool.view.recording.confirmation.yes=Si
 tool.view.recording.confirmation.no=No
 tool.view.activity=Actividad
 tool.view.description=Descripci&#243;n
+tool.view.preview=Vista preliminar
 tool.view.date=Fecha
 tool.view.duration=Duraci&#243;n
 tool.view.actions=Acciones
 tool.view.dateFormat=E, MMM dd, yyyy HH:mm:ss Z
 
-tool.error.general=No pudo estableserce la conexi&#243;n.
\ No newline at end of file
+tool.error.general=No pudo estableserce la conexi&#243;n.
diff --git a/bbb-lti/grails-app/i18n/messages_fr.properties b/bbb-lti/grails-app/i18n/messages_fr.properties
index 70c9fd7fbd..6d56c9e0d0 100644
--- a/bbb-lti/grails-app/i18n/messages_fr.properties
+++ b/bbb-lti/grails-app/i18n/messages_fr.properties
@@ -1,42 +1,43 @@
-#
-# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
-#
-# Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
-#
-# This program is free software; you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free Software
-# Foundation; either version 3.0 of the License, or (at your option) any later
-# version.
-#
-# BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
-# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
-#
-
-bigbluebutton.welcome.header=<br>Bienvenue au <b>{0}</b>!<br>
-bigbluebutton.welcome.footer=<br>Pour comprendre comment fonctionne BigBlueButton, consultez les <a href=\"event:http://www.bigbluebutton.org/content/videos\"><u>didacticiels vid&#233;o</u></a>.<br><br>Pour activer l'audio cliquez sur l'ic&#244;ne du casque &#224; &#233;couteurs (coin sup&#233;rieur gauche). <b>S'il vous pla�t utiliser le casque pour &#233;viter de causer du bruit.</b>
-
-tool.view.app=BigBlueButton
-tool.view.title=LTI Interface pour BigBlueButton
-tool.view.join=Saisie de la r&#233;union
-tool.view.recording=Enregistrement
-tool.view.recording.format.presentation=presentation
-tool.view.recording.format.video=video
-tool.view.recording.delete.confirmation=Veillez &#224; supprimer d&#233;finitivement cet enregistrement?
-tool.view.recording.delete.confirmation.warning=Attention
-tool.view.recording.delete.confirmation.yes=Oui
-tool.view.recording.delete.confirmation.no=Non
-tool.view.recording.publish=Publier
-tool.view.recording.unpublish=D&#233;publier
-tool.view.recording.delete=Supprimer
-tool.view.activity=Activit&#233;
-tool.view.description=Description
-tool.view.date=Date
-tool.view.duration=Dur&#233;e
-tool.view.actions=Actions
-tool.view.dateFormat=E, MMM dd, yyyy HH:mm:ss Z
-
-tool.error.general=Pas possible &#233;tablir la connection.
\ No newline at end of file
+#
+# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
+#
+# Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
+#
+# This program is free software; you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free Software
+# Foundation; either version 3.0 of the License, or (at your option) any later
+# version.
+#
+# BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
+#
+
+bigbluebutton.welcome.header=<br>Bienvenue au <b>{0}</b>!<br>
+bigbluebutton.welcome.footer=<br>Pour comprendre comment fonctionne BigBlueButton, consultez les <a href=\"event:http://www.bigbluebutton.org/content/videos\"><u>didacticiels vid&#233;o</u></a>.<br><br>Pour activer l'audio cliquez sur l'ic&#244;ne du casque &#224; &#233;couteurs (coin sup&#233;rieur gauche). <b>S'il vous pla�t utiliser le casque pour &#233;viter de causer du bruit.</b>
+
+tool.view.app=BigBlueButton
+tool.view.title=LTI Interface pour BigBlueButton
+tool.view.join=Saisie de la r&#233;union
+tool.view.recording=Enregistrement
+tool.view.recording.format.presentation=presentation
+tool.view.recording.format.video=video
+tool.view.recording.delete.confirmation=Veillez &#224; supprimer d&#233;finitivement cet enregistrement?
+tool.view.recording.delete.confirmation.warning=Attention
+tool.view.recording.delete.confirmation.yes=Oui
+tool.view.recording.delete.confirmation.no=Non
+tool.view.recording.publish=Publier
+tool.view.recording.unpublish=D&#233;publier
+tool.view.recording.delete=Supprimer
+tool.view.activity=Activit&#233;
+tool.view.description=Description
+tool.view.preview=Apre&#231;u
+tool.view.date=Date
+tool.view.duration=Dur&#233;e
+tool.view.actions=Actions
+tool.view.dateFormat=E, MMM dd, yyyy HH:mm:ss Z
+
+tool.error.general=Pas possible &#233;tablir la connection.
diff --git a/bbb-lti/grails-app/services/org/bigbluebutton/BigbluebuttonService.groovy b/bbb-lti/grails-app/services/org/bigbluebutton/BigbluebuttonService.groovy
index 297a6ce554..17714737b5 100644
--- a/bbb-lti/grails-app/services/org/bigbluebutton/BigbluebuttonService.groovy
+++ b/bbb-lti/grails-app/services/org/bigbluebutton/BigbluebuttonService.groovy
@@ -34,6 +34,10 @@ import javax.xml.parsers.DocumentBuilder;
 import javax.xml.parsers.DocumentBuilderFactory;
 import javax.xml.parsers.ParserConfigurationException;
 
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.json.XML;
 import org.w3c.dom.Document;
 import org.w3c.dom.Node;
 import org.w3c.dom.NodeList;
@@ -63,20 +67,20 @@ class BigbluebuttonService {
         try {
             docBuilder = docBuilderFactory.newDocumentBuilder()
         } catch (ParserConfigurationException e) {
-            logger.error("Failed to initialise BaseProxy", e)
+            log.error("Failed to initialise BaseProxy", e)
         }
-
         //Instantiate bbbProxy and initialize it with default url and salt
         bbbProxy = new Proxy(url, salt)
     }
 
     public String getJoinURL(params, welcome, mode){
-        //Set the injected values
-        if( !url.equals(bbbProxy.url) && !url.equals("") ) bbbProxy.setUrl(url)
-        if( !salt.equals(bbbProxy.salt) && !salt.equals("") ) bbbProxy.setSalt(salt)
-
-        String joinURL = null
-
+        // Set the injected values
+        if (!url.equals(bbbProxy.url) && !url.equals("")) {
+            bbbProxy.setUrl(url)
+        }
+        if (!salt.equals(bbbProxy.salt) && !salt.equals("")) {
+            bbbProxy.setSalt(salt)
+        }
         String meetingName = getValidatedMeetingName(params.get(Parameter.RESOURCE_LINK_TITLE))
         String meetingID = getValidatedMeetingId(params.get(Parameter.RESOURCE_LINK_ID), params.get(Parameter.CONSUMER_ID))
         String attendeePW = DigestUtils.shaHex("ap" + params.get(Parameter.RESOURCE_LINK_ID) + params.get(Parameter.CONSUMER_ID))
@@ -86,7 +90,6 @@ class BigbluebuttonService {
         String userFullName = getValidatedUserFullName(params, isModerator)
         String courseTitle = getValidatedCourseTitle(params.get(Parameter.COURSE_TITLE))
         String userID = getValidatedUserId(params.get(Parameter.USER_ID))
-
         Integer voiceBridge = 0
         String record = false
         Integer duration = 0
@@ -95,98 +98,93 @@ class BigbluebuttonService {
             record = getValidatedBBBRecord(params.get(Parameter.CUSTOM_RECORD)) || ltiService.allRecordedByDefault()
             duration = getValidatedBBBDuration(params.get(Parameter.CUSTOM_DURATION))
         }
-
         Boolean allModerators = Boolean.valueOf(false)
         if ( params.containsKey(Parameter.CUSTOM_ALL_MODERATORS) ) {
             allModerators = Boolean.parseBoolean(params.get(Parameter.CUSTOM_ALL_MODERATORS))
         }
-
         String[] values = [meetingName, courseTitle]
         String welcomeMsg = MessageFormat.format(welcome, values)
-
         String meta = getMonitoringMetaData(params)
-
-        String createURL = getCreateURL( meetingName, meetingID, attendeePW, moderatorPW, welcomeMsg, voiceBridge, logoutURL, record, duration, meta )
-        log.debug "createURL: " + createURL
-        Map<String, Object> createResponse = doAPICall(createURL)
-        log.debug "createResponse: " + createResponse
-
-        if( createResponse != null){
-            String returnCode = (String) createResponse.get("returncode")
-            String messageKey = (String) createResponse.get("messageKey")
-            if ( Proxy.APIRESPONSE_SUCCESS.equals(returnCode) ||
-                (Proxy.APIRESPONSE_FAILED.equals(returnCode) &&  (Proxy.MESSAGEKEY_IDNOTUNIQUE.equals(messageKey) || Proxy.MESSAGEKEY_DUPLICATEWARNING.equals(messageKey)) ) ){
-                joinURL = bbbProxy.getJoinURL( userFullName, meetingID, (isModerator || allModerators)? moderatorPW: attendeePW, (String) createResponse.get("createTime"), userID);
-            }
+        String createURL = getCreateURL(meetingName, meetingID, attendeePW, moderatorPW, welcomeMsg, voiceBridge, logoutURL, record, duration, meta)
+        Map<String, Object> responseAPICall = doAPICall(createURL)
+        log.info "responseAPICall: " + responseAPICall
+        if (responseAPICall == null) {
+            return null
         }
-
+        Object response = (Object)responseAPICall.get("response")
+        String returnCode = (String)response.get("returncode")
+        String messageKey = (String)response.get("messageKey")
+        if (!Proxy.APIRESPONSE_SUCCESS.equals(returnCode) ||
+            !Proxy.MESSAGEKEY_IDNOTUNIQUE.equals(messageKey) &&
+            !Proxy.MESSAGEKEY_DUPLICATEWARNING.equals(messageKey) &&
+            !"".equals(messageKey)) {
+            return null
+        }
+        def joinURL = bbbProxy.getJoinURL(userFullName, meetingID, (isModerator || allModerators)? moderatorPW: attendeePW, (String) response.get("createTime"), userID)
+        log.info "joinURL: " + joinURL
         return joinURL
     }
 
-    public Object getRecordings(params){
-        //Set the injected values
-        if( !url.equals(bbbProxy.url) && !url.equals("") ) bbbProxy.setUrl(url)
-        if( !salt.equals(bbbProxy.salt) && !salt.equals("") ) bbbProxy.setSalt(salt)
-
+    public Object getRecordings(params) {
+        // Set the injected values
+        if (!url.equals(bbbProxy.url) && !url.equals("")) {
+            bbbProxy.setUrl(url)
+        }
+        if (!salt.equals(bbbProxy.salt) && !salt.equals("")) {
+            bbbProxy.setSalt(salt)
+        }
         String meetingID = getValidatedMeetingId(params.get(Parameter.RESOURCE_LINK_ID), params.get(Parameter.CONSUMER_ID))
-
-        String recordingsURL = bbbProxy.getGetRecordingsURL( meetingID )
-        log.debug "recordingsURL: " + recordingsURL
-        Map<String, Object> recordings = doAPICall(recordingsURL)
-
-        if( recordings != null){
-            String returnCode = (String) recordings.get("returncode")
-            String messageKey = (String) recordings.get("messageKey")
-            if ( Proxy.APIRESPONSE_SUCCESS.equals(returnCode) && messageKey == null ){
-                return recordings.get("recordings")
-            }
+        String recordingsURL = bbbProxy.getGetRecordingsURL(meetingID)
+        Map<String, Object> responseAPICall = doAPICall(recordingsURL)
+        if (responseAPICall == null) {
+            return null
         }
-
-        return null
+        Object response = (Object)responseAPICall.get("response")
+        String returnCode = (String)response.get("returncode")
+        String messageKey = (String)response.get("messageKey")
+        if (!Proxy.APIRESPONSE_SUCCESS.equals(returnCode) || messageKey != null) {
+            return null
+        }
+        Object recordings = (Object)response.get("recordings")
+        return recordings
     }
 
     public Object doDeleteRecordings(params){
-        //Set the injected values
-        if( !url.equals(bbbProxy.url) && !url.equals("") ) bbbProxy.setUrl(url)
-        if( !salt.equals(bbbProxy.salt) && !salt.equals("") ) bbbProxy.setSalt(salt)
-
-        Map<String, Object> result
-
+        // Set the injected values
+        if (!url.equals(bbbProxy.url) && !url.equals("")) {
+            bbbProxy.setUrl(url)
+        }
+        if (!salt.equals(bbbProxy.salt) && !salt.equals("")) {
+            bbbProxy.setSalt(salt)
+        }
         String recordingId = getValidatedBBBRecordingId(params.get(Parameter.BBB_RECORDING_ID))
-
-        if( !recordingId.equals("") ){
+        if (!recordingId.equals("")) {
             String deleteRecordingsURL = bbbProxy.getDeleteRecordingsURL( recordingId )
-            log.debug "deleteRecordingsURL: " + deleteRecordingsURL
-            result = doAPICall(deleteRecordingsURL)
-        } else {
-            result = new HashMap<String, String>()
-            result.put("resultMessageKey", "InvalidRecordingId")
-            result.put("resultMessage", "RecordingId is invalid. The recording can not be deleted.")
+            return doAPICall(deleteRecordingsURL)
         }
-
+        def result = new HashMap<String, String>()
+        result.put("messageKey", "InvalidRecordingId")
+        result.put("message", "RecordingId is invalid. The recording can not be deleted.")
         return result
     }
 
     public Object doPublishRecordings(params){
-        //Set the injected values
-        if( !url.equals(bbbProxy.url) && !url.equals("") ) bbbProxy.setUrl(url)
-        if( !salt.equals(bbbProxy.salt) && !salt.equals("") ) bbbProxy.setSalt(salt)
-
-        Map<String, Object> result
-
+        // Set the injected values
+        if (!url.equals(bbbProxy.url) && !url.equals("")) {
+            bbbProxy.setUrl(url)
+        }
+        if (!salt.equals(bbbProxy.salt) && !salt.equals("")) {
+            bbbProxy.setSalt(salt)
+        }
         String recordingId = getValidatedBBBRecordingId(params.get(Parameter.BBB_RECORDING_ID))
         String publish = getValidatedBBBRecordingPublished(params.get(Parameter.BBB_RECORDING_PUBLISHED))
-
         if( !recordingId.equals("") ){
             String publishRecordingsURL = bbbProxy.getPublishRecordingsURL( recordingId, "true".equals(publish)?"false":"true" )
-            log.debug "publishRecordingsURL: " + publishRecordingsURL
-            result = doAPICall(publishRecordingsURL)
-        } else {
-            result = new HashMap<String, String>()
-            result.put("resultMessageKey", "InvalidRecordingId")
-            result.put("resultMessage", "RecordingId is invalid. The recording can not be deleted.")
+            return doAPICall(publishRecordingsURL)
         }
-
+        def result = new HashMap<String, String>()
+        result.put("messageKey", "InvalidRecordingId")
+        result.put("message", "RecordingId is invalid. The recording can not be deleted.")
         return result
     }
 
@@ -219,14 +217,14 @@ class BigbluebuttonService {
         String userFirstName = params.get(Parameter.USER_FIRSTNAME)
         String userLastName = params.get(Parameter.USER_LASTNAME)
         if( userFullName == null || userFullName == "" ){
-            if( userFirstName != null && userFirstName != "" ){
+            if (userFirstName != null && userFirstName != "") {
                 userFullName = userFirstName
             }
-            if( userLastName != null && userLastName != "" ){
+            if (userLastName != null && userLastName != "") {
                 userFullName += userFullName.length() > 0? " ": ""
                 userFullName += userLastName
             }
-            if( userFullName == null || userFullName == "" ){
+            if (userFullName == null || userFullName == "") {
                 userFullName = isModerator? "Moderator" : "Attendee"
             }
         }
@@ -263,8 +261,7 @@ class BigbluebuttonService {
 
     private String getMonitoringMetaData(params){
         String meta
-
-        meta = "meta_origin=" + bbbProxy.getStringEncoded(params.get(Parameter.TOOL_CONSUMER_CODE) == null? "": params.get(Parameter.TOOL_CONSUMER_CODE))
+        meta  = "meta_origin=" + bbbProxy.getStringEncoded(params.get(Parameter.TOOL_CONSUMER_CODE) == null? "": params.get(Parameter.TOOL_CONSUMER_CODE))
         meta += "&meta_originVersion=" + bbbProxy.getStringEncoded(params.get(Parameter.TOOL_CONSUMER_VERSION) == null? "": params.get(Parameter.TOOL_CONSUMER_VERSION))
         meta += "&meta_originServerCommonName=" + bbbProxy.getStringEncoded(params.get(Parameter.TOOL_CONSUMER_INSTANCE_DESCRIPTION) == null? "": params.get(Parameter.TOOL_CONSUMER_INSTANCE_DESCRIPTION))
         meta += "&meta_originServerUrl=" + bbbProxy.getStringEncoded(params.get(Parameter.TOOL_CONSUMER_INSTANCE_URL) == null? "": params.get(Parameter.TOOL_CONSUMER_INSTANCE_URL))
@@ -272,25 +269,21 @@ class BigbluebuttonService {
         meta += "&meta_contextId=" + bbbProxy.getStringEncoded(params.get(Parameter.COURSE_ID) == null? "": params.get(Parameter.COURSE_ID))
         meta += "&meta_contextActivity=" + bbbProxy.getStringEncoded(params.get(Parameter.RESOURCE_LINK_TITLE) == null? "": params.get(Parameter.RESOURCE_LINK_TITLE))
         meta += "&meta_contextActivityDescription=" + bbbProxy.getStringEncoded(params.get(Parameter.RESOURCE_LINK_DESCRIPTION) == null? "": params.get(Parameter.RESOURCE_LINK_DESCRIPTION))
-
         return meta
     }
 
     /** Make an API call */
     private Map<String, Object> doAPICall(String query) {
         StringBuilder urlStr = new StringBuilder(query);
-
         try {
             // open connection
-            //log.debug("doAPICall.call: " + query );
-
+            log.debug("doAPICall.call: " + query );
             URL url = new URL(urlStr.toString());
             HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection();
             httpConnection.setUseCaches(false);
             httpConnection.setDoOutput(true);
             httpConnection.setRequestMethod("GET");
             httpConnection.connect();
-
             int responseCode = httpConnection.getResponseCode();
             if (responseCode == HttpURLConnection.HTTP_OK) {
                 // read response
@@ -302,35 +295,27 @@ class BigbluebuttonService {
                     reader = new BufferedReader(isr);
                     String line = reader.readLine();
                     while (line != null) {
-                        if( !line.startsWith("<?xml version=\"1.0\"?>"))
+                        if( !line.startsWith("<?xml version=\"1.0\"?>")) {
                             xml.append(line.trim());
+                        }
                         line = reader.readLine();
                     }
                 } finally {
-                    if (reader != null)
+                    if (reader != null) {
                         reader.close();
-                    if (isr != null)
+                    }
+                    if (isr != null) {
                         isr.close();
+                    }
                 }
                 httpConnection.disconnect();
-
-                // parse response
+                // Parse response.
                 //log.debug("doAPICall.responseXml: " + xml);
                 //Patch to fix the NaN error
                 String stringXml = xml.toString();
                 stringXml = stringXml.replaceAll(">.\\s+?<", "><");
-
-                Document dom = null;
-                dom = docBuilder.parse(new InputSource( new StringReader(stringXml)));
-
-                Map<String, Object> response = getNodesAsMap(dom, "response");
-                //log.debug("doAPICall.responseMap: " + response);
-
-                String returnCode = (String) response.get("returncode");
-                if (Proxy.APIRESPONSE_FAILED.equals(returnCode)) {
-                    log.debug("doAPICall." + (String) response.get("messageKey") + ": Message=" + (String) response.get("message"));
-                }
-
+                JSONObject rootJSON = XML.toJSONObject(stringXml);
+                Map<String, Object> response = jsonToMap(rootJSON);
                 return response;
             } else {
                 log.debug("doAPICall.HTTPERROR: Message=" + "BBB server responded with HTTP status code " + responseCode);
@@ -346,43 +331,43 @@ class BigbluebuttonService {
         }
     }
 
-    /** Get all nodes under the specified element tag name as a Java map */
-    protected Map<String, Object> getNodesAsMap(Document dom, String elementTagName) {
-        Node firstNode = dom.getElementsByTagName(elementTagName).item(0);
-        return processNode(firstNode);
+    protected Map<String, Object> jsonToMap(JSONObject json) throws JSONException {
+        Map<String, Object> retMap = new HashMap<String, Object>();
+        if(json != JSONObject.NULL) {
+            retMap = toMap(json);
+        }
+        return retMap;
     }
 
-    protected Map<String, Object> processNode(Node _node) {
+    protected Map<String, Object> toMap(JSONObject object) throws JSONException {
         Map<String, Object> map = new HashMap<String, Object>();
-        NodeList responseNodes = _node.getChildNodes();
-        for (int i = 0; i < responseNodes.getLength(); i++) {
-            Node node = responseNodes.item(i);
-            String nodeName = node.getNodeName().trim();
-            if (node.getChildNodes().getLength() == 1
-                    && ( node.getChildNodes().item(0).getNodeType() == org.w3c.dom.Node.TEXT_NODE || node.getChildNodes().item(0).getNodeType() == org.w3c.dom.Node.CDATA_SECTION_NODE) ) {
-                String nodeValue = node.getTextContent();
-                map.put(nodeName, nodeValue != null ? nodeValue.trim() : null);
-
-            } else if (node.getChildNodes().getLength() == 0
-                    && node.getNodeType() != org.w3c.dom.Node.TEXT_NODE
-                    && node.getNodeType() != org.w3c.dom.Node.CDATA_SECTION_NODE) {
-                map.put(nodeName, "");
-
-            } else if ( node.getChildNodes().getLength() >= 1
-                    && node.getChildNodes().item(0).getChildNodes().item(0).getNodeType() != org.w3c.dom.Node.TEXT_NODE
-                    && node.getChildNodes().item(0).getChildNodes().item(0).getNodeType() != org.w3c.dom.Node.CDATA_SECTION_NODE ) {
-
-                List<Object> list = new ArrayList<Object>();
-                for (int c = 0; c < node.getChildNodes().getLength(); c++) {
-                    Node n = node.getChildNodes().item(c);
-                    list.add(processNode(n));
-                }
-                map.put(nodeName, list);
-
-            } else {
-                map.put(nodeName, processNode(node));
+        Iterator<String> keysItr = object.keys();
+        while(keysItr.hasNext()) {
+            String key = keysItr.next();
+            Object value = object.get(key);
+            if(value instanceof JSONArray) {
+                value = toList((JSONArray) value);
+            }
+            else if(value instanceof JSONObject) {
+                value = toMap((JSONObject) value);
             }
+            map.put(key, value);
         }
         return map;
     }
+
+    protected List<Object> toList(JSONArray array) throws JSONException {
+        List<Object> list = new ArrayList<Object>();
+        for(int i = 0; i < array.length(); i++) {
+            Object value = array.get(i);
+            if(value instanceof JSONArray) {
+                value = toList((JSONArray) value);
+            }
+            else if(value instanceof JSONObject) {
+                value = toMap((JSONObject) value);
+            }
+            list.add(value);
+        }
+        return list;
+    }
 }
diff --git a/bbb-lti/grails-app/services/org/bigbluebutton/LtiService.groovy b/bbb-lti/grails-app/services/org/bigbluebutton/LtiService.groovy
index c78c127ba0..2465c8e330 100644
--- a/bbb-lti/grails-app/services/org/bigbluebutton/LtiService.groovy
+++ b/bbb-lti/grails-app/services/org/bigbluebutton/LtiService.groovy
@@ -46,42 +46,37 @@ class LtiService {
 
     private Map<String, String> getConsumer(consumerId) {
         Map<String, String> consumer = null
-
-        if( this.consumerMap.containsKey(consumerId) ){
+        if (this.consumerMap.containsKey(consumerId)) {
             consumer = new HashMap<String, String>()
             consumer.put("key", consumerId);
             consumer.put("secret",  this.consumerMap.get(consumerId))
         }
-
         return consumer
     }
 
-    private void initConsumerMap(){
+    private void initConsumerMap() {
         this.consumerMap = new HashMap<String, String>()
         String[] consumers = this.consumers.split(",")
-        //for( int i=0; i < consumers.length; i++){
-        if ( consumers.length > 0 ){
+        if ( consumers.length > 0 ) {
             int i = 0;
             String[] consumer = consumers[i].split(":")
             if( consumer.length == 2 ){
                 this.consumerMap.put(consumer[0], consumer[1])
             }
         }
-
     }
 
-    public String sign(String sharedSecret, String data) throws Exception
-    {
+    public String sign(String sharedSecret, String data)
+        throws Exception {
         Mac mac = setKey(sharedSecret)
-
         // Signed String must be BASE64 encoded.
         byte[] signBytes = mac.doFinal(data.getBytes("UTF8"));
         String signature = encodeBase64(signBytes);
         return signature;
     }
 
-    private Mac setKey(String sharedSecret) throws Exception
-    {
+    private Mac setKey(String sharedSecret)
+        throws Exception {
         Mac mac = Mac.getInstance("HmacSHA1");
         byte[] keyBytes = sharedSecret.getBytes("UTF8");
         SecretKeySpec signingKey = new SecretKeySpec(keyBytes, "HmacSHA1");
@@ -110,7 +105,6 @@ class LtiService {
 
     def boolean isSSLEnabled(String query) {
         def ssl_enabled = false
-
         log.debug("Pinging SSL connection")
         try {
             // open connection
@@ -122,14 +116,12 @@ class LtiService {
             httpConnection.setRequestMethod("HEAD")
             httpConnection.setConnectTimeout(5000)
             httpConnection.connect()
-
             int responseCode = httpConnection.getResponseCode()
             if (responseCode == HttpURLConnection.HTTP_OK) {
                 ssl_enabled = true
             } else {
                 log.debug("HTTPERROR: Message=" + "BBB server responded with HTTP status code " + responseCode)
             }
-
         } catch(IOException e) {
             log.debug("IOException: Message=" + e.getMessage())
         } catch(IllegalArgumentException e) {
@@ -148,4 +140,8 @@ class LtiService {
     def boolean allRecordedByDefault() {
         return Boolean.parseBoolean(this.recordedByDefault);
     }
+
+    def String getScheme(request) {
+        return request.isSecure() ? "https" : "http"
+    }
 }
diff --git a/bbb-lti/grails-app/views/tool/error.gsp b/bbb-lti/grails-app/views/tool/error.gsp
index e9cc755530..e1442fa84d 100644
--- a/bbb-lti/grails-app/views/tool/error.gsp
+++ b/bbb-lti/grails-app/views/tool/error.gsp
@@ -1,35 +1,35 @@
-<html>
-  <head>
-    <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"/>
-    <title>Error</title>
-    <asset:stylesheet src="bootstrap.css"/>
-    <asset:stylesheet src="tool.css"/>
-    <asset:javascript src="jquery.js"/>
-    <asset:javascript src="bootstrap.js"/>
-  </head>
-  <body>
-    <div class="body">
-      <br/><br/>
-      <div class="container">
-      <g:if test="${ (resultMessageKey == 'InvalidEPortfolioUserId')}">
-        <div class="alert alert-warning">
-          ${resultMessage}
-        </div>
-      </g:if>
-      <g:else>
-        <div class="alert alert-danger">
-          <g:message code="tool.error.general" />
-        </div>
-      </g:else>
-      </div>
-    </div>
-    <!-- {
-            "error": {
-                "messageKey": "${resultMessageKey}",
-                "message": "${resultMessage}"
-            }
-        }
-    -->
-    <br/><br/>
-  </body>
+<html>
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"/>
+    <title>Error</title>
+    <asset:stylesheet src="bootstrap.css"/>
+    <asset:stylesheet src="tool.css"/>
+    <asset:javascript src="jquery.js"/>
+    <asset:javascript src="bootstrap.js"/>
+  </head>
+  <body>
+    <div class="body">
+      <br/><br/>
+      <div class="container">
+      <g:if test="${ (resultMessageKey == 'InvalidEPortfolioUserId')}">
+        <div class="alert alert-warning">
+          ${resultMessage}
+        </div>
+      </g:if>
+      <g:else>
+        <div class="alert alert-danger">
+          <g:message code="tool.error.general" />
+        </div>
+      </g:else>
+      </div>
+    </div>
+    <!-- {
+            "error": {
+                "messageKey": "${resultMessageKey}",
+                "message": "${resultMessage}"
+            }
+        }
+    -->
+    <br/><br/>
+  </body>
 </html>
\ No newline at end of file
diff --git a/bbb-lti/grails-app/views/tool/index.gsp b/bbb-lti/grails-app/views/tool/index.gsp
index 342e5a5284..c6b8526b94 100644
--- a/bbb-lti/grails-app/views/tool/index.gsp
+++ b/bbb-lti/grails-app/views/tool/index.gsp
@@ -1,77 +1,79 @@
-<html>
-    <head>
-        <title><g:message code="tool.view.title" /></title>
-        <link rel="shortcut icon" href="${assetPath(src: 'favicon.ico')}" type="image/x-icon">
-        <asset:stylesheet src="bootstrap.css"/>
-        <asset:stylesheet src="dataTables.bootstrap.min.css"/>
-        <asset:javascript src="jquery.js"/>
-        <asset:javascript src="jquery.dataTables.min.js"/>
-        <asset:javascript src="dataTables.bootstrap.min.js"/>
-        <asset:javascript src="dataTables.plugin.datetime.js"/>
-        <asset:javascript src="moment-with-locales.min.js"/>
-        <asset:javascript src="bootstrap.js"/>
-        <asset:javascript src="bootstrap-confirmation.min.js"/>
-        <asset:javascript src="tool.js"/>
-    </head>
-    <body>
-        <h1 style="margin-left:20px; text-align: center;"><a title="<g:message code="tool.view.join" />" class="btn btn-primary btn-large" href="${createLink(controller:'tool', action:'join', id: '0')}"><g:message code="tool.view.join" /></a></h1>
-        <br><br>
-        <div class="container">
-        <table id="recordings" class="table table-striped table-bordered dt-responsive" width="100%">
-            <thead>
-                <tr>
-                    <th class="header c0" style="text-align:center;" scope="col"><g:message code="tool.view.recording" /></th>
-                    <th class="header c1" style="text-align:center;" scope="col"><g:message code="tool.view.activity" /></th>
-                    <th class="header c2" style="text-align:center;" scope="col"><g:message code="tool.view.description" /></th>
-                    <th class="header c3" style="text-align:center;" scope="col"><g:message code="tool.view.date" /></th>
-                    <th class="header c4" style="text-align:center;" scope="col"><g:message code="tool.view.duration" /></th>
-                    <g:if test="${ismoderator}">
-                    <th class="header c5 lastcol" style="text-align:center;" scope="col"><g:message code="tool.view.actions" /></th>
-                    </g:if>
-                </tr>
-            </thead>
-            <tbody>
-            <g:each in="${recordingList}" var="r">
-                <g:if test="${ismoderator || r.published == 'true'}">  
-                <tr class="r0 lastrow">
-                    <td class="cell c0" style="text-align:center;">
-                    <g:if test="${r.published == 'true'}">
-                    <g:each in="${r.playback}" var="p">
-                        <a title="<g:message code="tool.view.recording.format.${p.type}" />" target="_new" href="${p.url}"><g:message code="tool.view.recording.format.${p.type}" /></a>&#32;
-                    </g:each>
-                    </g:if>
-                    </td>
-                    <td class="cell c1" style="text-align:center;">${r.name}</td>
-                    <td class="cell c2" style="text-align:center;">${r.metadata.contextactivitydescription}</td>
-                    <td class="cell c3" style="text-align:center;">${r.unixDate}</td>
-                    <td class="cell c4" style="text-align:center;">${r.duration}</td>
-                    <g:if test="${ismoderator}">
-                    <td class="cell c5 lastcol" style="text-align:center;">
-                      <g:if test="${r.published == 'true'}">
-                      <a title="<g:message code="tool.view.recording.unpublish" />" class="btn btn-default btn-sm glyphicon glyphicon-eye-close" name="unpublish_recording" type="submit" value="${r.recordID}" href="${createLink(controller:'tool',action:'publish',id: '0')}?bbb_recording_published=${r.published}&bbb_recording_id=${r.recordID}"></a>
-                      </g:if>
-                      <g:else>
-                      <a title="<g:message code="tool.view.recording.publish" />" class="btn btn-default btn-sm glyphicon glyphicon-eye-open" name="publish_recording" type="submit" value="${r.recordID}" href="${createLink(controller:'tool',action:'publish',id: '0')}?bbb_recording_published=${r.published}&bbb_recording_id=${r.recordID}"></a>
-                      </g:else>
-                      <a title="<g:message code="tool.view.recording.delete" />" class="btn btn-danger btn-sm glyphicon glyphicon-trash" name="delete_recording" value="${r.recordID}"
-                        data-toggle="confirmation"
-                        data-title="<g:message code="tool.view.recording.delete.confirmation.warning" />"
-                        data-content="<g:message code="tool.view.recording.delete.confirmation" />"
-                        data-btn-ok-label="<g:message code="tool.view.recording.delete.confirmation.yes" />"
-                        data-btn-cancel-label="<g:message code="tool.view.recording.delete.confirmation.no" />"
-                        data-placement="left"
-                        href="${createLink(controller:'tool',action:'delete',id: '0')}?bbb_recording_id=${r.recordID}">
-                      </a>
-                    </td>
-                    </g:if>
-                </tr>
-                </g:if>
-            </g:each>
-            </tbody>
-        </table>
-        </div>
-    </body>
-    <g:javascript>
-        var locale = '${params.launch_presentation_locale}';
-    </g:javascript>  
-</html>
\ No newline at end of file
+<html>
+    <head>
+        <title><g:message code="tool.view.title" /></title>
+        <link rel="shortcut icon" href="${assetPath(src: 'favicon.ico')}" type="image/x-icon">
+        <asset:stylesheet src="bootstrap.css"/>
+        <asset:stylesheet src="dataTables.bootstrap.min.css"/>
+        <asset:javascript src="jquery.js"/>
+        <asset:javascript src="jquery.dataTables.min.js"/>
+        <asset:javascript src="dataTables.bootstrap.min.js"/>
+        <asset:javascript src="dataTables.plugin.datetime.js"/>
+        <asset:javascript src="moment-with-locales.min.js"/>
+        <asset:javascript src="bootstrap.js"/>
+        <asset:javascript src="bootstrap-confirmation.min.js"/>
+        <asset:javascript src="tool.js"/>
+    </head>
+    <body>
+        <h1 style="margin-left:20px; text-align: center;"><a title="<g:message code="tool.view.join" />" class="btn btn-primary btn-large" href="${createLink(controller:'tool', action:'join', id: '0')}"><g:message code="tool.view.join" /></a></h1>
+        <br><br>
+        <div class="container">
+        <table id="recordings" class="table table-striped table-bordered dt-responsive" width="100%">
+            <thead>
+                <tr>
+                    <th class="header c0" style="text-align:center;" scope="col"><g:message code="tool.view.recording" /></th>
+                    <th class="header c1" style="text-align:center;" scope="col"><g:message code="tool.view.activity" /></th>
+                    <th class="header c2" style="text-align:center;" scope="col"><g:message code="tool.view.description" /></th>
+                    <th class="header c3" style="text-align:center;" scope="col"><g:message code="tool.view.preview" /></th>
+                    <th class="header c4" style="text-align:center;" scope="col"><g:message code="tool.view.date" /></th>
+                    <th class="header c5" style="text-align:center;" scope="col"><g:message code="tool.view.duration" /></th>
+                    <g:if test="${ismoderator}">
+                    <th class="header c6 lastcol" style="text-align:center;" scope="col"><g:message code="tool.view.actions" /></th>
+                    </g:if>
+                </tr>
+            </thead>
+            <tbody>
+            <g:each in="${recordingList}" var="r">
+                <g:if test="${ismoderator || r.published == 'true'}">
+                <tr class="r0 lastrow">
+                    <td class="cell c0" style="text-align:center;">
+                    <g:if test="${r.published}">
+                        <g:each in="${r.playback}" var="format">
+                            <a title="<g:message code="tool.view.recording.format.${format.getValue().type}" />" target="_new" href="${format.getValue().url}"><g:message code="tool.view.recording.format.${format.getValue().type}" /></a>&#32;
+                        </g:each>
+                    </g:if>
+                    </td>
+                    <td class="cell c1" style="text-align:left;">${r.name}</td>
+                    <td class="cell c2" style="text-align:left;">${r.metadata.contextactivitydescription}</td>
+                    <td class="cell c3" style="text-align:left;">${r.metadata.contextactivitydescription}</td>
+                    <td class="cell c4" style="text-align:left;">${r.reportDate}</td>
+                    <td class="cell c5" style="text-align:right;">${r.duration}</td>
+                    <g:if test="${ismoderator}">
+                    <td class="cell c6 lastcol" style="text-align:center;">
+                      <g:if test="${r.published == 'true'}">
+                      <a title="<g:message code="tool.view.recording.unpublish" />" class="btn btn-default btn-sm glyphicon glyphicon-eye-close" name="unpublish_recording" type="submit" value="${r.recordID}" href="${createLink(controller:'tool',action:'publish',id: '0')}?bbb_recording_published=${r.published}&bbb_recording_id=${r.recordID}"></a>
+                      </g:if>
+                      <g:else>
+                      <a title="<g:message code="tool.view.recording.publish" />" class="btn btn-default btn-sm glyphicon glyphicon-eye-open" name="publish_recording" type="submit" value="${r.recordID}" href="${createLink(controller:'tool',action:'publish',id: '0')}?bbb_recording_published=${r.published}&bbb_recording_id=${r.recordID}"></a>
+                      </g:else>
+                      <a title="<g:message code="tool.view.recording.delete" />" class="btn btn-danger btn-sm glyphicon glyphicon-trash" name="delete_recording" value="${r.recordID}"
+                        data-toggle="confirmation"
+                        data-title="<g:message code="tool.view.recording.delete.confirmation.warning" />"
+                        data-content="<g:message code="tool.view.recording.delete.confirmation" />"
+                        data-btn-ok-label="<g:message code="tool.view.recording.delete.confirmation.yes" />"
+                        data-btn-cancel-label="<g:message code="tool.view.recording.delete.confirmation.no" />"
+                        data-placement="left"
+                        href="${createLink(controller:'tool',action:'delete',id: '0')}?bbb_recording_id=${r.recordID}">
+                      </a>
+                    </td>
+                    </g:if>
+                </tr>
+                </g:if>
+            </g:each>
+            </tbody>
+        </table>
+        </div>
+    </body>
+    <g:javascript>
+        var locale = '${params.launch_presentation_locale}';
+    </g:javascript>
+</html>
-- 
GitLab