Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SAK-50890 LTI+CC left nav import create tool+content #13222

Merged
merged 7 commits into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,8 @@ private void addSupplementaryItemAttachments(Document doc, Element item, List<St

@Override
@Transactional
public String merge(String siteId, Element root, String archivePath, String fromSiteId, String creatorId, Map<String, String> attachmentNames, Map<String, String> userIdTrans, Set<String> userListAllowImport) {
public String merge(String siteId, Element root, String archivePath, String fromSiteId, String creatorId, Map<String, String> attachmentNames,
Map<Long, Map<String, Object>> ltiContentItems, Map<String, String> userIdTrans, Set<String> userListAllowImport) {

final StringBuilder results = new StringBuilder();
results.append("begin merging ").append(getLabel()).append(" context ").append(siteId).append(LINE_SEPARATOR);
Expand Down
4 changes: 4 additions & 0 deletions common/archive-impl/impl2/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
<dependency>
<groupId>org.sakaiproject.lti</groupId>
<artifactId>lti-api</artifactId>
</dependency>
</dependencies>
<build>
<resources />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import lombok.extern.slf4j.Slf4j;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.springframework.transaction.support.TransactionTemplate;
import org.w3c.dom.CDATASection;
import org.w3c.dom.Document;
Expand All @@ -44,6 +45,8 @@
import org.w3c.dom.Node;
import org.w3c.dom.NamedNodeMap;

import org.apache.commons.lang3.StringUtils;

import org.sakaiproject.archive.api.ArchiveService;
import org.sakaiproject.authz.api.AuthzGroup;
import org.sakaiproject.authz.api.GroupNotDefinedException;
Expand All @@ -60,11 +63,14 @@
import org.sakaiproject.user.api.User;
import org.sakaiproject.user.api.UserDirectoryService;
import org.sakaiproject.content.api.ContentHostingService;
import org.sakaiproject.lti.api.LTIService;
import org.sakaiproject.util.Xml;

@Slf4j
public class SiteArchiver {

@Setter private LTIService ltiService;

/** Dependency: ServerConfigurationService. */
protected ServerConfigurationService m_serverConfigurationService = null;
public void setServerConfigurationService(ServerConfigurationService service) {
Expand Down Expand Up @@ -309,36 +315,47 @@ protected String archiveSite(Site site, Document doc, Stack stack, String fromSy
Element siteNode = site.toXml(doc, stack);

// By default, do not include fields that have secret or password in the name
String filter = m_serverConfigurationService.getString("archive.toolproperties.excludefilter","password|secret");
String filter = m_serverConfigurationService.getString("archive.toolproperties.excludefilter","password|secret");
Pattern pattern = null;
if ( ( ! "none".equals(filter) ) && filter.length() > 0 ) {
try {
if ( ( ! "none".equals(filter) ) && filter.length() > 0 ) {
try {
pattern = Pattern.compile(filter);
}
catch (Exception e) {
pattern = null;
}
}

if ( pattern != null ) {
NodeList nl = siteNode.getElementsByTagName("property");
List<Element> toRemove = new ArrayList<Element>();
NodeList nl = siteNode.getElementsByTagName("property");
List<Element> toRemove = new ArrayList<Element>();

for(int i = 0; i < nl.getLength(); i++) {
Element proptag = (Element)nl.item(i);
String propname = proptag.getAttribute("name");
if ( propname == null ) continue;
propname = propname.toLowerCase();
for(int i = 0; i < nl.getLength(); i++) {
Element proptag = (Element)nl.item(i);
String propname = proptag.getAttribute("name");
if ( StringUtils.isEmpty(propname) ) continue;
propname = propname.toLowerCase();
if ( pattern != null ) {
Matcher matcher = pattern.matcher(propname);
if ( matcher.find() ) {
toRemove.add(proptag);
continue;
}
}
for(Element proptag : toRemove ) {
proptag.getParentNode().removeChild(proptag);

if ( propname.equals("source") ) {
String propvalue = Xml.decodeAttribute(proptag, "value");
Long contentKey = ltiService.getContentKeyFromLaunch(propvalue);
if ( contentKey > 0 ) {
Element contentElement = ltiService.archiveContentByKey(doc, contentKey, site.getId());
// Attach to the <tool> tag
if ( contentElement != null ) proptag.getParentNode().getParentNode().appendChild(contentElement);
}
}
}

for(Element proptag : toRemove ) {
proptag.getParentNode().removeChild(proptag);
}

stack.push(siteNode);

String realmId = m_siteService.siteReference(site.getId());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@
import java.util.Map;
import java.util.Vector;

import java.util.regex.Pattern;
import java.util.regex.Matcher;

import lombok.extern.slf4j.Slf4j;
import lombok.Setter;

import org.apache.commons.codec.binary.Base64;

Expand All @@ -49,6 +53,7 @@
import org.sakaiproject.exception.IdUsedException;
import org.sakaiproject.exception.InUseException;
import org.sakaiproject.exception.PermissionException;
import org.sakaiproject.lti.api.LTIService;
import org.sakaiproject.site.api.Site;
import org.sakaiproject.site.api.SiteService;
import org.sakaiproject.user.api.UserDirectoryService;
Expand All @@ -58,10 +63,13 @@
@Slf4j
public class SiteMerger {
protected static HashMap userIdTrans = new HashMap();

/**********************************************/
/* Injected Dependencies */
/**********************************************/

@Setter private LTIService ltiService;

protected AuthzGroupService m_authzGroupService = null;
public void setAuthzGroupService(AuthzGroupService service) {
m_authzGroupService = service;
Expand Down Expand Up @@ -92,7 +100,6 @@ public void setServerConfigurationService(ServerConfigurationService m_serverCon
this.m_serverConfigurationService = m_serverConfigurationService;
}


// only the resources created by the followinng roles will be imported
// role sets are different to different system
//public String[] SAKAI_roles = m_filteredSakaiRoles; //= {"Affiliate", "Assistant", "Instructor", "Maintain", "Organizer", "Owner"};
Expand Down Expand Up @@ -156,6 +163,7 @@ public String merge(String fileName, String siteId, String creatorId, String m_s

// track old to new attachment names
Map attachmentNames = new HashMap();
Map<Long, Map<String, Object>> ltiContentItems = new HashMap();

// The archive.xml is really a debug log, not actual archive data - it does not participate in any merge
for (int i = 0; i < files.length; i++)
Expand All @@ -167,12 +175,23 @@ public String merge(String fileName, String siteId, String creatorId, String m_s
}
}

// Pull in the ltiContentItems for use in later merges
for (int i = 0; i < files.length; i++)
{
if ((files[i] != null) && (files[i].getPath().indexOf("basiclti.xml") != -1))
{
processLti(files[i].getPath(), siteId, results, ltiContentItems);
files[i] = null;
break;
}
}

// firstly, merge the users
for (int i = 0; i < files.length; i++)
{
if ((files[i] != null) && (files[i].getPath().indexOf("user.xml") != -1))
{
processMerge(files[i].getPath(), siteId, results, attachmentNames, null, filterSakaiServices, filteredSakaiServices, filterSakaiRoles, filteredSakaiRoles);
processMerge(files[i].getPath(), siteId, results, attachmentNames, ltiContentItems, null, filterSakaiServices, filteredSakaiServices, filterSakaiRoles, filteredSakaiRoles);
files[i] = null;
break;
}
Expand All @@ -185,7 +204,6 @@ public String merge(String fileName, String siteId, String creatorId, String m_s
if ((files[i] != null) && (files[i].getPath().indexOf("site.xml") != -1))
{
siteFile = files[i].getPath();
processMerge(files[i].getPath(), siteId, results, attachmentNames, creatorId, filterSakaiServices, filteredSakaiServices, filterSakaiRoles, filteredSakaiRoles);
files[i] = null;
break;
}
Expand All @@ -196,7 +214,7 @@ public String merge(String fileName, String siteId, String creatorId, String m_s
{
if ((files[i] != null) && (files[i].getPath().indexOf("attachment.xml") != -1))
{
processMerge(files[i].getPath(), siteId, results, attachmentNames, null, filterSakaiServices, filteredSakaiServices, filterSakaiRoles, filteredSakaiRoles);
processMerge(files[i].getPath(), siteId, results, attachmentNames, ltiContentItems, null, filterSakaiServices, filteredSakaiServices, filterSakaiRoles, filteredSakaiRoles);
files[i] = null;
break;
}
Expand All @@ -208,13 +226,13 @@ public String merge(String fileName, String siteId, String creatorId, String m_s
if (files[i] != null)
if (files[i].getPath().endsWith(".xml"))
{
processMerge(files[i].getPath(), siteId, results, attachmentNames, creatorId, filterSakaiServices, filteredSakaiServices, filterSakaiRoles, filteredSakaiRoles);
processMerge(files[i].getPath(), siteId, results, attachmentNames, ltiContentItems, creatorId, filterSakaiServices, filteredSakaiServices, filterSakaiRoles, filteredSakaiRoles);
}
}

if (siteFile != null )
{
processMerge(siteFile, siteId, results, attachmentNames, creatorId, filterSakaiServices, filteredSakaiServices, filterSakaiRoles, filteredSakaiRoles);
processMerge(siteFile, siteId, results, attachmentNames, ltiContentItems, creatorId, filterSakaiServices, filteredSakaiServices, filterSakaiRoles, filteredSakaiRoles);
}

return results.toString();
Expand All @@ -227,10 +245,11 @@ public String merge(String fileName, String siteId, String creatorId, String m_s
* @param siteId The id of the site to merge the content into.
* @param results A buffer to accumulate result messages.
* @param attachmentNames A map of old to new attachment names.
* @param ltiContentItems A map of LTI Content Items associated with this import
* @param useIdTrans A map of old WorkTools id to new Ctools id
* @param creatorId The creator id
*/
protected void processMerge(String fileName, String siteId, StringBuilder results, Map attachmentNames, String creatorId, boolean filterSakaiService, String[] filteredSakaiService, boolean filterSakaiRoles, String[] filteredSakaiRoles)
protected void processMerge(String fileName, String siteId, StringBuilder results, Map attachmentNames, Map<Long, Map<String, Object>> ltiContentItems, String creatorId, boolean filterSakaiService, String[] filteredSakaiService, boolean filterSakaiRoles, String[] filteredSakaiRoles)
{
// correct for windows backslashes
fileName = fileName.replace('\\', '/');
Expand Down Expand Up @@ -283,7 +302,7 @@ protected void processMerge(String fileName, String siteId, StringBuilder result
//if (system.equalsIgnoreCase(ArchiveService.FROM_WT))
// mergeSite(siteId, fromSite, element, userIdTrans, creatorId);
//else
mergeSite(siteId, fromSite, element, new HashMap()/*empty userIdMap */, creatorId, filterSakaiRoles, filteredSakaiRoles);
mergeSite(siteId, fromSite, element, new HashMap()/*empty userIdMap */, creatorId, ltiContentItems, filterSakaiRoles, filteredSakaiRoles);
}
else if (element.getTagName().equals(UserDirectoryService.APPLICATION_ID))
{ ;
Expand Down Expand Up @@ -338,7 +357,7 @@ else if (element.getTagName().equals(UserDirectoryService.APPLICATION_ID))
if (checkSakaiService(filterSakaiService, filteredSakaiService, serviceName)) {
// checks passed so now we attempt to do the merge
log.debug("Merging archive data for {} ({}) to site {}", serviceName, fileName, siteId);
msg = service.merge(siteId, element, fileName, fromSite, creatorId, attachmentNames, new HashMap() /* empty userIdTran map */, usersListAllowImport);
msg = service.merge(siteId, element, fileName, fromSite, creatorId, attachmentNames, ltiContentItems, new HashMap() /* empty userIdTran map */, usersListAllowImport);
} else {
log.warn("Skipping merge archive data for "+serviceName+" ("+fileName+") to site "+siteId+", checked filter failed (filtersOn="+filterSakaiService+", filters="+Arrays.toString(filteredSakaiService)+")");
}
Expand All @@ -365,6 +384,69 @@ else if (element.getTagName().equals(UserDirectoryService.APPLICATION_ID))
}

} // processMerge

/**
* Read in the archive file and pull out Content Items
* @param fileName The site name (for the archive file) to read from.
* @param siteId The id of the site to merge the content into.
* @param results A buffer to accumulate result messages.
* @param ltiContentItems A map of LTI Content Items associated with this import
*/
protected void processLti(String fileName, String siteId, StringBuilder results, Map<Long, Map<String, Object>> ltiContentItems)
{
// correct for windows backslashes
fileName = fileName.replace('\\', '/');

if (log.isDebugEnabled())
log.debug("merge(): processing file: " + fileName);

// read the whole file into a DOM
Document doc = Xml.readDocument(fileName);
if (doc == null)
{
results.append("Error reading xml from: " + fileName + "\n");
return;
}

// verify the root element
Element root = doc.getDocumentElement();
if (!root.getTagName().equals("archive"))
{
results.append("File: " + fileName + " does not contain archive xml. Found this root tag: " + root.getTagName() + "\n");
return;
}

// the children lti tools ARCHIVE_LTI_CONTENT_TAG
NodeList contentNodes = root.getElementsByTagName(LTIService.ARCHIVE_LTI_CONTENT_TAG);
final int length = contentNodes.getLength();
for(int i = 0; i < length; i++)
{
Node contentNode = contentNodes.item(i);
if (contentNode.getNodeType() != Node.ELEMENT_NODE) continue;
Element contentElement = (Element)contentNode;

Map<String, Object> content = new HashMap();
Map<String, Object> tool = new HashMap();
ltiService.mergeContent(contentElement, content, tool);
String contentErrors = ltiService.validateContent(content);
if ( contentErrors != null ) {
log.warn("import found invalid content tag {}", contentErrors);
continue;
}

String toolErrors = ltiService.validateTool(tool);
if ( toolErrors != null ) {
log.warn("import found invalid tool tag {}", toolErrors);
continue;
}
Long contentId = ltiService.getId(content);
if ( contentId > 0 ) {
content.put("TOOL_IMPORT", tool);
ltiContentItems.put(contentId, content);
}
}

} // processLti

/**
* Merge the site definition from the site part of the archive file into the site service.
Expand All @@ -373,11 +455,12 @@ else if (element.getTagName().equals(UserDirectoryService.APPLICATION_ID))
* @param fromSiteId The id of the site the archive was made from.
* @param element The XML DOM tree of messages to merge.
* @param creatorId The creator id
* @param ltiContentItems A map of LTI Content Items associated with this import
*/
protected void mergeSite(String siteId, String fromSiteId, Element element, HashMap useIdTrans, String creatorId, boolean filterSakaiRoles, String[] filteredSakaiRoles)
protected void mergeSite(String siteId, String fromSiteId, Element element, HashMap useIdTrans, String creatorId, Map<Long, Map<String,Object>> ltiContentItems, boolean filterSakaiRoles, String[] filteredSakaiRoles)
{
String source = "";

Node parent = element.getParentNode();
if (parent.getNodeType() == Node.ELEMENT_NODE)
{
Expand Down Expand Up @@ -409,6 +492,36 @@ protected void mergeSite(String siteId, String fromSiteId, Element element, Hash
}
}
element3.setAttribute("toolId", toolId);

// If this is a sakai.web.168 that launches an LTI url
// import the associated content item and tool and re-link them
// to the tool placement
if ( LTIService.WEB_PORTLET.equals(toolId) ) {
Element foundProperty = null;
NodeList propertyChildren = element3.getElementsByTagName("property");
for(int i3 = 0; i3 < propertyChildren.getLength(); i3++)
{
Element propElement = (Element)propertyChildren.item(i3);
String propname = propElement.getAttribute("name");
if ( "source".equals(propname) ) {
String propvalue = Xml.decodeAttribute(propElement, "value");
if ( ltiService.getContentKeyFromLaunch(propvalue) > 0 ) {
foundProperty = propElement;
break;
}
}
}

if ( foundProperty != null ) {
Long contentKey = ltiService.mergeContentFromImport(element, siteId);
if ( contentKey != null ) {
Map<String, Object> theContent = ltiService.getContent(contentKey, siteId);
String launchUrl = ltiService.getContentLaunch(theContent);
Xml.encodeAttribute(foundProperty, "value", launchUrl);
}
}
}

}

// merge the site info first
Expand Down
2 changes: 2 additions & 0 deletions common/archive-impl/impl2/src/webapp/WEB-INF/components.xml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
<property name="authzGroupService" ref="org.sakaiproject.authz.api.AuthzGroupService" />
<property name="userDirectoryService" ref="org.sakaiproject.user.api.UserDirectoryService" />
<property name="timeService" ref="org.sakaiproject.time.api.TimeService" />
<property name="ltiService" ref="org.sakaiproject.lti.api.LTIService" />
<property name="contentHostingService" ref="org.sakaiproject.content.api.ContentHostingService" />
<property name="transactionTemplate">
<bean class="org.springframework.transaction.support.TransactionTemplate">
Expand All @@ -71,6 +72,7 @@
<property name="userDirectoryService" ref="org.sakaiproject.user.api.UserDirectoryService" />
<property name="securityService" ref="org.sakaiproject.authz.api.SecurityService" />
<property name="entityManager" ref="org.sakaiproject.entity.api.EntityManager" />
<property name="ltiService" ref="org.sakaiproject.lti.api.LTIService" />
<property name="serverConfigurationService" ref="org.sakaiproject.component.api.ServerConfigurationService"/>
</bean>

Expand Down
Loading
Loading