/** * <a href="http://www.openolat.org"> * OpenOLAT - Online Learning and Training</a><br> * <p> * Licensed under the Apache License, Version 2.0 (the "License"); <br> * you may not use this file except in compliance with the License.<br> * You may obtain a copy of the License at the * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> * <p> * Unless required by applicable law or agreed to in writing,<br> * software distributed under the License is distributed on an "AS IS" BASIS, <br> * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> * See the License for the specific language governing permissions and <br> * limitations under the License. * <p> * Initial code contributed and copyrighted by<br> * frentix GmbH, http://www.frentix.com * <p> */ package org.olat.ims.lti.manager; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.stream.Collectors; import javax.persistence.TypedQuery; import org.apache.commons.io.IOUtils; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpPost; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.message.BasicNameValuePair; import org.imsglobal.basiclti.BasicLTIUtil; import org.olat.basesecurity.Authentication; import org.olat.basesecurity.BaseSecurityManager; import org.olat.core.commons.persistence.DB; import org.olat.core.helpers.Settings; import org.olat.core.id.Identity; import org.olat.core.id.User; import org.olat.core.id.UserConstants; import org.olat.core.logging.OLog; import org.olat.core.logging.Tracing; import org.olat.core.util.StringHelper; import org.olat.core.util.WebappHelper; import org.olat.ims.lti.LTIContext; import org.olat.ims.lti.LTIManager; import org.olat.ims.lti.LTIOutcome; import org.olat.ims.lti.model.LTIOutcomeImpl; import org.olat.ldap.ui.LDAPAuthenticationController; import org.olat.resource.OLATResource; import org.olat.shibboleth.ShibbolethDispatcher; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; /** * * Initial date: 13.05.2013<br> * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com * */ @Service public class LTIManagerImpl implements LTIManager { private static final OLog log = Tracing.createLoggerFor(LTIManagerImpl.class); @Autowired private DB dbInstance; @Override public LTIOutcome createOutcome(Identity identity, OLATResource resource, String resSubPath, String action, String outcomeKey, String outcomeValue) { LTIOutcomeImpl outcome = new LTIOutcomeImpl(); outcome.setAssessedIdentity(identity); outcome.setResource(resource); if(StringHelper.containsNonWhitespace(resSubPath)) { outcome.setResSubPath(resSubPath); } outcome.setCreationDate(new Date()); outcome.setLastModified(new Date()); outcome.setAction(action); outcome.setOutcomeKey(outcomeKey); outcome.setOutcomeValue(outcomeValue); dbInstance.getCurrentEntityManager().persist(outcome); return outcome; } @Override public LTIOutcome loadOutcomeByKey(Long key) { List<LTIOutcome> outcomes = dbInstance.getCurrentEntityManager() .createNamedQuery("loadLTIOutcomeByKey", LTIOutcome.class). setParameter("outcomeKey", key) .getResultList(); if(outcomes.isEmpty()) { return null; } return outcomes.get(0); } @Override public List<LTIOutcome> loadOutcomes(Identity identity, OLATResource resource, String resSubPath) { StringBuilder sb = new StringBuilder(); sb.append("select outcome from ltioutcome outcome where outcome.assessedIdentity.key=:identityKey and outcome.resource=:resource"); if(StringHelper.containsNonWhitespace(resSubPath)) { sb.append(" and outcome.resSubPath=:resSubPath"); } else { sb.append(" and outcome.resSubPath is null"); } TypedQuery<LTIOutcome> outcomes = dbInstance.getCurrentEntityManager() .createQuery(sb.toString(), LTIOutcome.class) .setParameter("identityKey", identity.getKey()) .setParameter("resource", resource); if(StringHelper.containsNonWhitespace(resSubPath)) { outcomes.setParameter("resSubPath", resSubPath); } return outcomes.getResultList(); } @Override public void deleteOutcomes(OLATResource resource) { String q = "delete from ltioutcome as outcome where outcome.resource=:resource"; dbInstance.getCurrentEntityManager().createQuery(q) .setParameter("resource", resource) .executeUpdate(); } @Override public Map<String,String> sign(Map<String,String> props, String url, String oauthKey, String oauthSecret) { String oauth_consumer_key = oauthKey; String oauth_consumer_secret = oauthSecret; String tool_consumer_instance_guid = Settings.getServerDomainName(); String tool_consumer_instance_description = null; String tool_consumer_instance_url = null; String tool_consumer_instance_name = WebappHelper.getInstanceId(); String tool_consumer_instance_contact_email = WebappHelper.getMailConfig("mailSupport"); if (props == null) { props = new HashMap<>(); } return BasicLTIUtil.signProperties(props, url, "POST", oauth_consumer_key, oauth_consumer_secret, tool_consumer_instance_guid, tool_consumer_instance_description, tool_consumer_instance_url, tool_consumer_instance_name, tool_consumer_instance_contact_email); } @Override public Map<String,String> forgeLTIProperties(Identity identity, Locale locale, LTIContext context, boolean sendName, boolean sendEmail) { final Locale loc = locale; final Identity ident = identity; final User u = ident.getUser(); final String lastName = u.getProperty(UserConstants.LASTNAME, loc); final String firstName = u.getProperty(UserConstants.FIRSTNAME, loc); final String email = u.getProperty(UserConstants.EMAIL, loc); Map<String,String> props = new HashMap<>(); setProperty(props, "resource_link_id", context.getResourceId()); setProperty(props, "resource_link_title", context.getResourceTitle()); setProperty(props, "resource_link_description", context.getResourceDescription()); //launch setProperty(props, "launch_presentation_locale", loc.toString()); setProperty(props, "launch_presentation_document_target", context.getTarget()); setProperty(props, "launch_presentation_return_url", context.getTalkBackMapperUri()); if(StringHelper.containsNonWhitespace(context.getPreferredWidth())) { setProperty(props, "launch_presentation_width", context.getPreferredWidth()); } if(StringHelper.containsNonWhitespace(context.getPreferredHeight())) { setProperty(props, "launch_presentation_height", context.getPreferredHeight()); } //consumer infos setProperty(props, "tool_consumer_info_product_family_code", "openolat"); setProperty(props, "tool_consumer_info_version", Settings.getVersion()); //outcome if(StringHelper.containsNonWhitespace(context.getOutcomeMapperUri())) { //setProperty(props, "ext_ims_lis_basic_outcome_url", context.getOutcomeMapperUri()); //setProperty(props, "ext_ims_lis_resultvalue_sourcedids", "decimal"); setProperty(props, "lis_result_sourcedid", context.getSourcedId()); setProperty(props, "lis_outcome_service_url", context.getOutcomeMapperUri()); } //user data setProperty(props, "user_id", u.getKey().toString()); setProperty(props, "lis_person_sourcedid", createPersonSourceId(identity)); if (sendName) { setProperty(props, "lis_person_name_given", firstName); setProperty(props, "lis_person_name_family", lastName); setProperty(props, "lis_person_name_full", firstName+" "+lastName); } if (sendEmail) { setProperty(props, "lis_person_contact_email_primary", email); } setProperty(props, "roles", context.getRoles(identity)); setProperty(props, "context_id", context.getContextId()); setProperty(props, "context_label", context.getContextTitle()); setProperty(props, "context_title", context.getContextTitle()); setProperty(props, "context_type", "CourseSection"); setCustomProperties(context.getCustomProperties(), identity, props); return props; } private void setCustomProperties(String custom, Identity identity, Map<String,String> props) { if (!StringHelper.containsNonWhitespace(custom)) return; String[] params = custom.split("[\n;]"); for (int i = 0; i < params.length; i++) { String param = params[i]; if (!StringHelper.containsNonWhitespace(param)) { continue; } int pos = param.indexOf('='); if (pos < 1 || pos + 1 > param.length()) { continue; } String key = BasicLTIUtil.mapKeyName(param.substring(0, pos)); if(!StringHelper.containsNonWhitespace(key)) { continue; } String value = param.substring(pos + 1).trim(); if(value.length() < 1) { continue; } if(value.startsWith(LTIManager.USER_PROPS_PREFIX)) { String userProp = value.substring(LTIManager.USER_PROPS_PREFIX.length(), value.length()); value = identity.getUser().getProperty(userProp, null); } setProperty(props, "custom_" + key, value); } } public void setProperty(Map<String,String> props, String key, String value) { if (value == null) return; if (value.trim().length() < 1) return; props.put(key, value); } /** * A comma-separated list of URN values for roles. If this list is non-empty, * it should contain at least one role from the LIS System Role, LIS * Institution Role, or LIS Context Role vocabularies (See Appendix A of * LTI_BasicLTI_Implementation_Guide_rev1.pdf). * * @param roles * @return */ /*private String setRoles(Identity identity, Roles roles, LTIContext context) { StringBuilder rolesStr; if (roles.isGuestOnly()) { rolesStr = new StringBuilder("Guest"); } else { rolesStr = new StringBuilder("Learner"); boolean coach = context.isCoach(identity); if (coach) { rolesStr.append(",").append("Instructor"); } boolean admin = context.isAdmin(identity); if (roles.isOLATAdmin() || admin) { rolesStr.append(",").append("Administrator"); } } return rolesStr.toString(); }*/ private String createPersonSourceId(Identity identity) { // The person source ID is used as user identifier. The rule is as follows: // 1) if a shibboleth authentication token is availble, use the ShibbolethModule.getDefaultUIDAttribute() // 2) if a LDAP authentication token is available, use the LDAPConstants.LDAP_USER_IDENTIFYER // 3) as fallback use the system URL together with the identity username String personSourceId = null; // Use the shibboleth ID as person source identificator List<Authentication> authMethods = BaseSecurityManager.getInstance().getAuthentications(identity); for (Authentication method : authMethods) { String provider = method.getProvider(); if (ShibbolethDispatcher.PROVIDER_SHIB.equals(provider)) { personSourceId = method.getAuthusername(); // done, case 1) break; } else if (LDAPAuthenticationController.PROVIDER_LDAP.equals(provider)) { personSourceId = method.getAuthusername(); // normally done, case 2). however, lets continue because we might still find a case 1) } // ignore all other authentication providers } if (!StringHelper.containsNonWhitespace(personSourceId)) { // fallback to the serverDomainName:identityId as case 3) personSourceId = Settings.getServerDomainName() + ":" + identity.getKey(); } return personSourceId; } @Override public String post(Map<String,String> signedProps, String url) { String content = null; // Map the LTI properties to HttpClient parameters List<NameValuePair> urlParameters = signedProps.keySet().stream() .map(k -> new BasicNameValuePair(k, signedProps.get(k))) .collect(Collectors.toList()); // make the http request and evaluate the result try (CloseableHttpClient httpclient = HttpClients.createDefault()) { HttpPost request = new HttpPost(url); HttpEntity postParams = new UrlEncodedFormEntity(urlParameters); request.setEntity(postParams); HttpResponse httpResponse = httpclient.execute(request); content = IOUtils.toString(httpResponse.getEntity().getContent()); } catch (Exception e) { log.error("", e); } return content; } }