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
@@ -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
0 commit comments