142142import org.sakaiproject.importer.api.SakaiArchive;
143143import org.sakaiproject.javax.PagingPosition;
144144import org.sakaiproject.lti.api.LTIService;
145+ import org.tsugi.lti.LTIUtil;
145146import org.sakaiproject.lti.util.SakaiLTIUtil;
146147import org.sakaiproject.memory.api.Cache;
147148import 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
0 commit comments