Skip to content

Commit 964be56

Browse files
committed
SAK-52083 LTI Stealthed tool deployment removal enhancement
1 parent 239323a commit 964be56

File tree

3 files changed

+214
-5
lines changed

3 files changed

+214
-5
lines changed

site-manage/site-manage-tool/tool/src/bundle/sitesetupgeneric.properties

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,9 @@ java.recmsg = Messages & Discussions Notifications
162162
java.roleperm = You do not have permission to add or remove user(s) with role ''{0}''.
163163
java.none = None
164164

165+
sitesetup.stranded.header = Missing LTI Tools
166+
sitesetup.stranded.message = These LTI tools are deployed in the site but are no longer available. They will be deleted when the site is saved.
167+
165168
#General Vm
166169
gen.alert = Alert:
167170
gen.continue = Continue

site-manage/site-manage-tool/tool/src/java/org/sakaiproject/site/tool/SiteAction.java

Lines changed: 196 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@
142142
import org.sakaiproject.importer.api.SakaiArchive;
143143
import org.sakaiproject.javax.PagingPosition;
144144
import org.sakaiproject.lti.api.LTIService;
145+
import org.tsugi.lti.LTIUtil;
145146
import org.sakaiproject.lti.util.SakaiLTIUtil;
146147
import org.sakaiproject.memory.api.Cache;
147148
import org.sakaiproject.memory.api.MemoryService;
@@ -795,6 +796,8 @@ public class SiteAction extends PagedResourceActionII {
795796
private static final String STATE_LTITOOL_EXISTING_SELECTED_LIST = "state_ltitool_existing_selected_list";
796797
// state variable for selected lti tools during tool modification
797798
private static final String STATE_LTITOOL_SELECTED_LIST = "state_ltitool_selected_list";
799+
// state variable for stranded lti tools (deployed in site but no longer available)
800+
private static final String STATE_LTITOOL_STRANDED_LIST = "state_tool_stranded_lti_tool_list";
798801
// special prefix String for basiclti tool ids
799802
private static final String LTITOOL_ID_PREFIX = "lti_";
800803

@@ -4197,6 +4200,15 @@ private void buildLTIToolContextForTemplate(Context context,
41974200
SessionState state, Site site, boolean updateToolRegistration) {
41984201
List<Map<String, Object>> visibleTools, allTools;
41994202
String siteId = site == null? UUID.randomUUID().toString(): site.getId();
4203+
4204+
// Determine if course navigation placement is required
4205+
boolean requireCourseNavPlacement = serverConfigurationService.getBoolean("site-manage.requireCourseNavPlacement", true);
4206+
4207+
// Get stranded LTI tools (deployed in site but no longer available)
4208+
List<MyTool> strandedTools = getStrandedLTITools(site, requireCourseNavPlacement);
4209+
state.setAttribute(STATE_LTITOOL_STRANDED_LIST, strandedTools);
4210+
context.put("strandedLtiTools", strandedTools);
4211+
42004212
// get the list of launchable tools - visible and including stealthed
42014213
visibleTools = ltiService.getToolsLaunch(siteId, true);
42024214
if (site == null) {
@@ -4234,7 +4246,7 @@ private void buildLTIToolContextForTemplate(Context context,
42344246
}
42354247
}
42364248
}
4237-
4249+
42384250
// First search list of visibleTools for those not selected (excluding stealthed tools)
42394251
for (Map<String, Object> toolMap : visibleTools ) {
42404252
String ltiToolId = toolMap.get("id").toString();
@@ -4247,7 +4259,7 @@ private void buildLTIToolContextForTemplate(Context context,
42474259
ltiTools.put(ltiToolId, toolMap);
42484260
}
42494261
}
4250-
4262+
42514263
// Second search list of allTools for those already selected (including stealthed)
42524264
for (Map<String, Object> toolMap : allTools ) {
42534265
String ltiToolId = toolMap.get("id").toString();
@@ -4258,8 +4270,7 @@ private void buildLTIToolContextForTemplate(Context context,
42584270
ltiTools.put(ltiToolId, toolMap);
42594271
}
42604272
}
4261-
4262-
4273+
42634274
state.setAttribute(STATE_LTITOOL_LIST, ltiTools);
42644275
state.setAttribute(STATE_LTITOOL_EXISTING_SELECTED_LIST, linkedLtiContents);
42654276
context.put("ltiTools", ltiTools);
@@ -6829,7 +6840,88 @@ private List<MyTool> getUngroupedTools(String ungroupedName, Map<String, List<My
68296840
}
68306841
return ungroupedTools;
68316842
}
6832-
6843+
6844+
/**
6845+
* Get list of LTI tools that are deployed in a site but no longer appear in the available tools list
6846+
* (stranded tools). This can happen when tools are stealthed, deleted, or have restrictions changed
6847+
* after being deployed to sites.
6848+
*
6849+
* @param site The site to check for stranded tools
6850+
* @param requireCourseNavPlacement Limit tools to those that have Course Navigation placement indicated
6851+
* @return List of MyTool objects representing stranded tools
6852+
*/
6853+
private List<MyTool> getStrandedLTITools(Site site, boolean requireCourseNavPlacement) {
6854+
List<MyTool> strandedTools = new ArrayList<>();
6855+
if (site == null) {
6856+
return strandedTools;
6857+
}
6858+
6859+
String siteId = site.getId();
6860+
List<String> ltiSelectedTools = selectedLTITools(site);
6861+
6862+
// Get the list of currently available LTI tools
6863+
List<Map<String, Object>> allTools;
6864+
if (requireCourseNavPlacement) {
6865+
allTools = ltiService.getToolsLaunchCourseNav(siteId, false);
6866+
} else {
6867+
allTools = ltiService.getToolsLaunch(siteId, true);
6868+
}
6869+
6870+
// Build a set of all available tool IDs for efficient lookup
6871+
Set<String> allToolIds = new HashSet<>();
6872+
if (allTools != null) {
6873+
for (Map<String, Object> tool : allTools) {
6874+
String toolIdString = ObjectUtils.toString(tool.get(LTIService.LTI_ID));
6875+
allToolIds.add(toolIdString);
6876+
}
6877+
}
6878+
6879+
// Find tools that are selected but not in the allTools list
6880+
List<String> missingToolIds = new ArrayList<>();
6881+
for (String selectedToolId : ltiSelectedTools) {
6882+
if (!allToolIds.contains(selectedToolId)) {
6883+
missingToolIds.add(selectedToolId);
6884+
}
6885+
}
6886+
6887+
// Build MyTool objects for each stranded tool
6888+
if (!missingToolIds.isEmpty()) {
6889+
log.debug("Found {} stranded LTI tools in site {} not in available tools list: {}",
6890+
missingToolIds.size(), siteId, missingToolIds);
6891+
6892+
for (String missingToolId : missingToolIds) {
6893+
try {
6894+
Map<String, Object> toolInfo = ltiService.getToolDao(Long.valueOf(missingToolId), siteId);
6895+
if (toolInfo != null) {
6896+
String title = ObjectUtils.toString(toolInfo.get("title"), "Unknown");
6897+
String visible = ObjectUtils.toString(toolInfo.get(LTIService.LTI_VISIBLE), "0");
6898+
String description = ObjectUtils.toString(toolInfo.get("description"), "");
6899+
6900+
log.debug("Stranded tool ID {}: title='{}', visible='{}', site_id='{}'",
6901+
missingToolId, title, visible, siteId);
6902+
6903+
// Create a MyTool for this stranded tool
6904+
MyTool strandedTool = new MyTool();
6905+
strandedTool.title = title;
6906+
strandedTool.id = LTITOOL_ID_PREFIX + missingToolId;
6907+
strandedTool.description = description;
6908+
strandedTool.selected = true; // It's in the site
6909+
strandedTool.required = false;
6910+
strandedTools.add(strandedTool);
6911+
} else {
6912+
log.debug("Stranded tool ID {}: Unable to retrieve tool information (tool may have been deleted)",
6913+
missingToolId);
6914+
}
6915+
} catch (Exception e) {
6916+
log.debug("Stranded tool ID {}: Error retrieving tool information: {}",
6917+
missingToolId, e.getMessage());
6918+
}
6919+
}
6920+
}
6921+
6922+
return strandedTools;
6923+
}
6924+
68336925
/* SAK 16600 Create list of ltitools to add to toolgroups; set selected for those
68346926
// tools already added to a sites with properties read to add to toolsByGroup list
68356927
* @param groupName name of the current group
@@ -12232,8 +12324,107 @@ else if (multiAllowed && toolId.startsWith(toolRegId))
1223212324
state.removeAttribute(STATE_TOOL_EMAIL_ADDRESS);
1223312325
}
1223412326

12327+
// Commit the site to save all tool/page changes made above
1223512328
commitSite(site);
1223612329

12330+
// Remove stranded LTI tools (tools deployed in site but no longer available)
12331+
// This is done AFTER committing the site so that:
12332+
// 1. All changes above are safely saved first
12333+
// 2. deleteContent() will save its own changes (removing pages)
12334+
// 3. We can then refresh to pick up those deletions without losing other changes
12335+
List<MyTool> strandedLtiTools = (List<MyTool>) state.getAttribute(STATE_LTITOOL_STRANDED_LIST);
12336+
if (strandedLtiTools != null && !strandedLtiTools.isEmpty()) {
12337+
String siteId = site.getId();
12338+
int totalStrandedTools = strandedLtiTools.size();
12339+
int successfulDeletions = 0;
12340+
int failedDeletions = 0;
12341+
int totalContentItemsFound = 0;
12342+
12343+
log.debug("saveFeatures: Starting cleanup of {} stranded LTI tools in site {}", totalStrandedTools, siteId);
12344+
12345+
for (MyTool stranded : strandedLtiTools) {
12346+
try {
12347+
String originalToolIdString = stranded.id;
12348+
String toolIdString = originalToolIdString;
12349+
12350+
log.debug("saveFeatures: Processing stranded tool - id='{}', title='{}', description='{}'",
12351+
originalToolIdString, stranded.title, stranded.description);
12352+
12353+
// Strip the prefix if present
12354+
if (toolIdString != null && toolIdString.startsWith(LTITOOL_ID_PREFIX)) {
12355+
toolIdString = toolIdString.substring(LTITOOL_ID_PREFIX.length());
12356+
log.debug("saveFeatures: Stripped prefix, numeric tool ID: {}", toolIdString);
12357+
}
12358+
12359+
Long toolId = Long.valueOf(toolIdString);
12360+
12361+
// Find all content items for this tool in this site and delete them
12362+
String searchClause = "lti_content.tool_id = " + toolId;
12363+
log.debug("saveFeatures: Searching for content items with query: {}", searchClause);
12364+
12365+
List<Map<String, Object>> contents = ltiService.getContentsDao(searchClause, null, 0, 5000, siteId, ltiService.isAdmin(siteId));
12366+
int contentCount = contents != null ? contents.size() : 0;
12367+
totalContentItemsFound += contentCount;
12368+
12369+
log.debug("saveFeatures: Found {} content item(s) for stranded tool {} in site {}",
12370+
contentCount, toolId, siteId);
12371+
12372+
if (contents != null) {
12373+
for (Map<String, Object> content : contents) {
12374+
Object contentIdObj = content.get(LTIService.LTI_ID);
12375+
if (contentIdObj != null) {
12376+
Long contentId = Long.valueOf(contentIdObj.toString());
12377+
String contentTitle = content.get(LTIService.LTI_TITLE) != null ?
12378+
content.get(LTIService.LTI_TITLE).toString() : "Untitled";
12379+
String placementId = content.get(LTIService.LTI_PLACEMENT) != null ?
12380+
content.get(LTIService.LTI_PLACEMENT).toString() : "null";
12381+
12382+
log.debug("saveFeatures: Attempting to delete content - id={}, title='{}', placement={}, toolId={}, siteId={}",
12383+
contentId, contentTitle, placementId, toolId, siteId);
12384+
12385+
boolean deleted = ltiService.deleteContent(contentId, siteId);
12386+
12387+
if (deleted) {
12388+
successfulDeletions++;
12389+
log.debug("saveFeatures: Successfully deleted stranded LTI content {} ('{}') for tool {} in site {}",
12390+
contentId, contentTitle, toolId, siteId);
12391+
} else {
12392+
failedDeletions++;
12393+
log.warn("saveFeatures: FAILED to delete stranded LTI content {} ('{}') for tool {} in site {} - deleteContent returned false",
12394+
contentId, contentTitle, toolId, siteId);
12395+
}
12396+
} else {
12397+
log.warn("saveFeatures: Content item missing LTI_ID field, cannot delete. Content map keys: {}",
12398+
content.keySet());
12399+
}
12400+
}
12401+
}
12402+
} catch (NumberFormatException e) {
12403+
failedDeletions++;
12404+
log.error("saveFeatures: NumberFormatException processing stranded LTI tool '{}' in site {}: {}",
12405+
stranded.id, site.getId(), e.getMessage(), e);
12406+
} catch (Exception e) {
12407+
failedDeletions++;
12408+
log.error("saveFeatures: Exception processing stranded LTI tool '{}' ('{}') in site {}: {}",
12409+
stranded.id, stranded.title, site.getId(), e.getMessage(), e);
12410+
}
12411+
}
12412+
12413+
// Clear after processing so we don't process again on subsequent saves
12414+
state.removeAttribute(STATE_LTITOOL_STRANDED_LIST);
12415+
12416+
// Log summary
12417+
log.debug("saveFeatures: Stranded LTI tool cleanup complete for site {} - {} tools processed, {} content items found, {} successful deletions, {} failed deletions",
12418+
siteId, totalStrandedTools, totalContentItemsFound, successfulDeletions, failedDeletions);
12419+
12420+
// Refresh the site object to pick up the page deletions made by deleteContent()
12421+
// The deleteContent() call internally saved the site, so we just need to reload our object
12422+
site = refreshSiteObject(site);
12423+
log.debug("saveFeatures: Site object refreshed after stranded LTI tool cleanup for site {}", siteId);
12424+
} else {
12425+
log.debug("saveFeatures: No stranded LTI tools found in state for site {}", site.getId());
12426+
}
12427+
1223712428
Map<String, Map<String, List<String>>> toolOptions = (Map<String, Map<String, List<String>>>) state.getAttribute(STATE_IMPORT_SITE_TOOL_OPTIONS);
1223812429
Map<String, Map<String, List<String>>> toolItemMap = (Map<String, Map<String, List<String>>>) state.getAttribute(STATE_IMPORT_SITE_TOOL_ITEMS);
1223912430

site-manage/site-manage-tool/tool/src/webapp/vm/sitesetup/toolGroupMultipleDisplay.vm

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,21 @@
55
});
66
</script>
77

8+
## Display stranded LTI tools
9+
#if ($strandedLtiTools && $strandedLtiTools.size() > 0)
10+
<div class="alert alert-warning" role="alert" style="margin: 15px 0;">
11+
<h4 class="alert-heading">
12+
<i class="fa fa-exclamation-triangle" style="margin-right: 8px;"></i>$tlang.getString("sitesetup.stranded.header")
13+
</h4>
14+
<p>$tlang.getString("sitesetup.stranded.message")</p>
15+
<ul style="margin-bottom: 0;">
16+
#foreach($tool in $strandedLtiTools)
17+
<li><strong>$formattedText.escapeHtml($tool.title)</strong>#if ($tool.description && !$tool.description.trim().isEmpty()) - $formattedText.escapeHtml($tool.description)#end</li>
18+
#end
19+
</ul>
20+
</div>
21+
#end
22+
823
#macro(toolListControl $tool $toolRegistrationSelectedList $ltiTools $allowPageOrderHelper $toolRegistrationTitleList)
924
#set($toolId = $tool.id)
1025
<div class="toolListControl" id="$toolId.replace(".","_")_wrap">

0 commit comments

Comments
 (0)