Skip to content

Commit 7b0c9b5

Browse files
committed
SAK-52083 LTI Stealthed tool deployment removal enhancement
1 parent c00e80d commit 7b0c9b5

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

@@ -4189,6 +4192,15 @@ private void buildLTIToolContextForTemplate(Context context,
41894192
SessionState state, Site site, boolean updateToolRegistration) {
41904193
List<Map<String, Object>> visibleTools, allTools;
41914194
String siteId = site == null? UUID.randomUUID().toString(): site.getId();
4195+
4196+
// Determine if course navigation placement is required
4197+
boolean requireCourseNavPlacement = serverConfigurationService.getBoolean("site-manage.requireCourseNavPlacement", true);
4198+
4199+
// Get stranded LTI tools (deployed in site but no longer available)
4200+
List<MyTool> strandedTools = getStrandedLTITools(site, requireCourseNavPlacement);
4201+
state.setAttribute(STATE_LTITOOL_STRANDED_LIST, strandedTools);
4202+
context.put("strandedLtiTools", strandedTools);
4203+
41924204
// get the list of launchable tools - visible and including stealthed
41934205
visibleTools = ltiService.getToolsLaunch(siteId, true);
41944206
if (site == null) {
@@ -4226,7 +4238,7 @@ private void buildLTIToolContextForTemplate(Context context,
42264238
}
42274239
}
42284240
}
4229-
4241+
42304242
// First search list of visibleTools for those not selected (excluding stealthed tools)
42314243
for (Map<String, Object> toolMap : visibleTools ) {
42324244
String ltiToolId = toolMap.get("id").toString();
@@ -4239,7 +4251,7 @@ private void buildLTIToolContextForTemplate(Context context,
42394251
ltiTools.put(ltiToolId, toolMap);
42404252
}
42414253
}
4242-
4254+
42434255
// Second search list of allTools for those already selected (including stealthed)
42444256
for (Map<String, Object> toolMap : allTools ) {
42454257
String ltiToolId = toolMap.get("id").toString();
@@ -4250,8 +4262,7 @@ private void buildLTIToolContextForTemplate(Context context,
42504262
ltiTools.put(ltiToolId, toolMap);
42514263
}
42524264
}
4253-
4254-
4265+
42554266
state.setAttribute(STATE_LTITOOL_LIST, ltiTools);
42564267
state.setAttribute(STATE_LTITOOL_EXISTING_SELECTED_LIST, linkedLtiContents);
42574268
context.put("ltiTools", ltiTools);
@@ -6821,7 +6832,88 @@ private List<MyTool> getUngroupedTools(String ungroupedName, Map<String, List<My
68216832
}
68226833
return ungroupedTools;
68236834
}
6824-
6835+
6836+
/**
6837+
* Get list of LTI tools that are deployed in a site but no longer appear in the available tools list
6838+
* (stranded tools). This can happen when tools are stealthed, deleted, or have restrictions changed
6839+
* after being deployed to sites.
6840+
*
6841+
* @param site The site to check for stranded tools
6842+
* @param requireCourseNavPlacement Limit tools to those that have Course Navigation placement indicated
6843+
* @return List of MyTool objects representing stranded tools
6844+
*/
6845+
private List<MyTool> getStrandedLTITools(Site site, boolean requireCourseNavPlacement) {
6846+
List<MyTool> strandedTools = new ArrayList<>();
6847+
if (site == null) {
6848+
return strandedTools;
6849+
}
6850+
6851+
String siteId = site.getId();
6852+
List<String> ltiSelectedTools = selectedLTITools(site);
6853+
6854+
// Get the list of currently available LTI tools
6855+
List<Map<String, Object>> allTools;
6856+
if (requireCourseNavPlacement) {
6857+
allTools = ltiService.getToolsLaunchCourseNav(siteId, false);
6858+
} else {
6859+
allTools = ltiService.getToolsLaunch(siteId, true);
6860+
}
6861+
6862+
// Build a set of all available tool IDs for efficient lookup
6863+
Set<String> allToolIds = new HashSet<>();
6864+
if (allTools != null) {
6865+
for (Map<String, Object> tool : allTools) {
6866+
String toolIdString = ObjectUtils.toString(tool.get(LTIService.LTI_ID));
6867+
allToolIds.add(toolIdString);
6868+
}
6869+
}
6870+
6871+
// Find tools that are selected but not in the allTools list
6872+
List<String> missingToolIds = new ArrayList<>();
6873+
for (String selectedToolId : ltiSelectedTools) {
6874+
if (!allToolIds.contains(selectedToolId)) {
6875+
missingToolIds.add(selectedToolId);
6876+
}
6877+
}
6878+
6879+
// Build MyTool objects for each stranded tool
6880+
if (!missingToolIds.isEmpty()) {
6881+
log.debug("Found {} stranded LTI tools in site {} not in available tools list: {}",
6882+
missingToolIds.size(), siteId, missingToolIds);
6883+
6884+
for (String missingToolId : missingToolIds) {
6885+
try {
6886+
Map<String, Object> toolInfo = ltiService.getToolDao(Long.valueOf(missingToolId), siteId);
6887+
if (toolInfo != null) {
6888+
String title = ObjectUtils.toString(toolInfo.get("title"), "Unknown");
6889+
String visible = ObjectUtils.toString(toolInfo.get(LTIService.LTI_VISIBLE), "0");
6890+
String description = ObjectUtils.toString(toolInfo.get("description"), "");
6891+
6892+
log.debug("Stranded tool ID {}: title='{}', visible='{}', site_id='{}'",
6893+
missingToolId, title, visible, siteId);
6894+
6895+
// Create a MyTool for this stranded tool
6896+
MyTool strandedTool = new MyTool();
6897+
strandedTool.title = title;
6898+
strandedTool.id = LTITOOL_ID_PREFIX + missingToolId;
6899+
strandedTool.description = description;
6900+
strandedTool.selected = true; // It's in the site
6901+
strandedTool.required = false;
6902+
strandedTools.add(strandedTool);
6903+
} else {
6904+
log.debug("Stranded tool ID {}: Unable to retrieve tool information (tool may have been deleted)",
6905+
missingToolId);
6906+
}
6907+
} catch (Exception e) {
6908+
log.debug("Stranded tool ID {}: Error retrieving tool information: {}",
6909+
missingToolId, e.getMessage());
6910+
}
6911+
}
6912+
}
6913+
6914+
return strandedTools;
6915+
}
6916+
68256917
/* SAK 16600 Create list of ltitools to add to toolgroups; set selected for those
68266918
// tools already added to a sites with properties read to add to toolsByGroup list
68276919
* @param groupName name of the current group
@@ -12224,8 +12316,107 @@ else if (multiAllowed && toolId.startsWith(toolRegId))
1222412316
state.removeAttribute(STATE_TOOL_EMAIL_ADDRESS);
1222512317
}
1222612318

12319+
// Commit the site to save all tool/page changes made above
1222712320
commitSite(site);
1222812321

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

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)