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ón +tool.view.preview=Vista preliminar tool.view.date=Fecha tool.view.duration=Duración tool.view.actions=Acciones tool.view.dateFormat=E, MMM dd, yyyy HH:mm:ss Z -tool.error.general=No pudo estableserce la conexión. \ No newline at end of file +tool.error.general=No pudo estableserce la conexió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éo</u></a>.<br><br>Pour activer l'audio cliquez sur l'icône du casque à écouteurs (coin supérieur gauche). <b>S'il vous pla�t utiliser le casque pour éviter de causer du bruit.</b> - -tool.view.app=BigBlueButton -tool.view.title=LTI Interface pour BigBlueButton -tool.view.join=Saisie de la réunion -tool.view.recording=Enregistrement -tool.view.recording.format.presentation=presentation -tool.view.recording.format.video=video -tool.view.recording.delete.confirmation=Veillez à supprimer dé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épublier -tool.view.recording.delete=Supprimer -tool.view.activity=Activité -tool.view.description=Description -tool.view.date=Date -tool.view.duration=Durée -tool.view.actions=Actions -tool.view.dateFormat=E, MMM dd, yyyy HH:mm:ss Z - -tool.error.general=Pas possible é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éo</u></a>.<br><br>Pour activer l'audio cliquez sur l'icône du casque à écouteurs (coin supérieur gauche). <b>S'il vous pla�t utiliser le casque pour éviter de causer du bruit.</b> + +tool.view.app=BigBlueButton +tool.view.title=LTI Interface pour BigBlueButton +tool.view.join=Saisie de la réunion +tool.view.recording=Enregistrement +tool.view.recording.format.presentation=presentation +tool.view.recording.format.video=video +tool.view.recording.delete.confirmation=Veillez à supprimer dé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épublier +tool.view.recording.delete=Supprimer +tool.view.activity=Activité +tool.view.description=Description +tool.view.preview=Apreçu +tool.view.date=Date +tool.view.duration=Durée +tool.view.actions=Actions +tool.view.dateFormat=E, MMM dd, yyyy HH:mm:ss Z + +tool.error.general=Pas possible é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>  - </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>  + </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