Skip to content
Draft
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
8 changes: 8 additions & 0 deletions calendar/calendar-impl/impl/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,14 @@
<version>${sakai.powermock.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.sakaiproject.search</groupId>
<artifactId>search-api</artifactId>
</dependency>
<dependency>
<groupId>org.sakaiproject.search</groupId>
<artifactId>search-util</artifactId>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,364 @@
/**********************************************************************************
* $URL$
* $Id$
***********************************************************************************
*
* Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008 The Sakai Foundation
*
* Licensed under the Educational Community License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.opensource.org/licenses/ECL-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
**********************************************************************************/

package org.sakaiproject.calendar.impl;

import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.sakaiproject.calendar.api.Calendar;
import org.sakaiproject.calendar.api.CalendarEvent;
import org.sakaiproject.calendar.api.CalendarService;
import org.sakaiproject.component.api.ServerConfigurationService;
import org.sakaiproject.entity.api.EntityManager;
import org.sakaiproject.entity.api.EntityProducer;
import org.sakaiproject.entity.api.Reference;
import org.sakaiproject.event.api.Event;
import org.sakaiproject.exception.IdUnusedException;
import org.sakaiproject.exception.PermissionException;
import org.sakaiproject.search.api.EntityContentProducer;
import org.sakaiproject.search.api.SearchIndexBuilder;
import org.sakaiproject.search.api.SearchService;
import org.sakaiproject.search.api.SearchUtils;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Wrong package for SearchUtils import (compile-time issue)

SearchUtils resides in search-util, typically under org.sakaiproject.search.util.SearchUtils, not org.sakaiproject.search.api.

Apply:

-import org.sakaiproject.search.api.SearchUtils;
+import org.sakaiproject.search.util.SearchUtils;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import org.sakaiproject.search.api.SearchUtils;
import org.sakaiproject.search.util.SearchUtils;
🤖 Prompt for AI Agents
In
calendar/calendar-impl/impl/src/java/org/sakaiproject/calendar/impl/CalendarContentProducer.java
around line 43, the file currently imports SearchUtils from the wrong package;
replace the incorrect import org.sakaiproject.search.api.SearchUtils with the
correct package org.sakaiproject.search.util.SearchUtils (remove the old import
and add the new one) so the class compiles against the search-util module.

import org.sakaiproject.search.model.SearchBuilderItem;
import org.sakaiproject.site.api.Site;
import org.sakaiproject.site.api.SiteService;
import org.sakaiproject.site.api.ToolConfiguration;

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

/**
* Content producer for calendar search functionality
*
* @author Generated for calendar search support
*/
@Slf4j
public class CalendarContentProducer implements EntityContentProducer {

@Setter @Getter
private SearchService searchService = null;

@Setter @Getter
private SearchIndexBuilder searchIndexBuilder = null;

@Setter @Getter
private EntityManager entityManager = null;

@Setter @Getter
private CalendarService calendarService = null;

@Setter @Getter
private List<String> addEvents = new ArrayList<>();

@Setter @Getter
private List<String> removeEvents = new ArrayList<>();

@Setter
private SiteService siteService;

@Setter
private ServerConfigurationService serverConfigurationService;

protected void init() throws Exception {
log.info("init()");

if ("true".equals(serverConfigurationService.getString("search.enable", "false"))) {
for (Iterator<String> i = addEvents.iterator(); i.hasNext();) {
getSearchService().registerFunction((String) i.next());
}
for (Iterator<String> i = removeEvents.iterator(); i.hasNext();) {
getSearchService().registerFunction((String) i.next());
}
getSearchIndexBuilder().registerEntityContentProducer(this);
}
}
Comment on lines +85 to +97
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use boolean config and guard service nulls in init()

Avoid string comparisons and NPEs if services aren’t injected (defensive).

-    if ("true".equals(serverConfigurationService.getString("search.enable", "false"))) {
+    if (serverConfigurationService != null
+        && serverConfigurationService.getBoolean("search.enable", false)
+        && searchService != null
+        && searchIndexBuilder != null) {
         for (Iterator<String> i = addEvents.iterator(); i.hasNext();) {
             getSearchService().registerFunction((String) i.next());
         }
         for (Iterator<String> i = removeEvents.iterator(); i.hasNext();) {
             getSearchService().registerFunction((String) i.next());
         }
         getSearchIndexBuilder().registerEntityContentProducer(this);
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
protected void init() throws Exception {
log.info("init()");
if ("true".equals(serverConfigurationService.getString("search.enable", "false"))) {
for (Iterator<String> i = addEvents.iterator(); i.hasNext();) {
getSearchService().registerFunction((String) i.next());
}
for (Iterator<String> i = removeEvents.iterator(); i.hasNext();) {
getSearchService().registerFunction((String) i.next());
}
getSearchIndexBuilder().registerEntityContentProducer(this);
}
}
protected void init() throws Exception {
log.info("init()");
if (serverConfigurationService != null
&& serverConfigurationService.getBoolean("search.enable", false)
&& searchService != null
&& searchIndexBuilder != null) {
for (Iterator<String> i = addEvents.iterator(); i.hasNext();) {
getSearchService().registerFunction((String) i.next());
}
for (Iterator<String> i = removeEvents.iterator(); i.hasNext();) {
getSearchService().registerFunction((String) i.next());
}
getSearchIndexBuilder().registerEntityContentProducer(this);
}
}


/**
* Destroy
*/
protected void destroy() {
log.info("destroy()");
}

private Reference getReference(String reference) {
try {
return entityManager.newReference(reference);
} catch (Exception ex) {
return null;
}
}

private EntityProducer getProducer(Reference ref) {
try {
return ref.getEntityProducer();
} catch (Exception ex) {
return null;
}
}

@Override
public boolean canRead(String reference) {
Reference ref = getReference(reference);
if (ref == null) return false;

CalendarEvent event = getCalendarEvent(ref);
if (event == null) return false;

String siteId = event.getSiteId();
try {
// Check if user can access the site - this is how calendar tool checks access
siteService.getSiteVisit(siteId);
return true;
} catch (Exception e) {
// User cannot access the site
return false;
}
}

@Override
public Integer getAction(Event event) {
String evt = event.getEvent();
if (evt == null) return SearchBuilderItem.ACTION_UNKNOWN;

for (Iterator<String> i = addEvents.iterator(); i.hasNext();) {
String match = (String) i.next();
if (evt.equals(match)) {
return SearchBuilderItem.ACTION_ADD;
}
}
for (Iterator<String> i = removeEvents.iterator(); i.hasNext();) {
String match = (String) i.next();
if (evt.equals(match)) {
return SearchBuilderItem.ACTION_DELETE;
}
}
return SearchBuilderItem.ACTION_UNKNOWN;
}

@Override
public String getContainer(String reference) {
try {
return getReference(reference).getContainer();
} catch (Exception ex) {
return "";
}
}

@Override
public String getContent(String reference) {
Reference ref = getReference(reference);
if (ref == null) return "";

CalendarEvent event = getCalendarEvent(ref);
if (event == null) return "";

StringBuilder sb = new StringBuilder();

// Add the display name
String displayName = event.getDisplayName();
if (displayName != null) {
SearchUtils.appendCleanString(displayName, sb);
sb.append("\n");
}

// Add the description
String description = event.getDescription();
if (description != null) {
SearchUtils.appendCleanString(description, sb);
sb.append("\n");
}

// Add the location
String location = event.getLocation();
if (location != null) {
SearchUtils.appendCleanString(location, sb);
sb.append("\n");
}

// Add the type
String type = event.getType();
if (type != null) {
SearchUtils.appendCleanString(type, sb);
}

log.debug("Calendar Event Content for reference: {} is: {}", ref.getReference(), sb.toString());
return sb.toString();
}
Comment on lines +207 to +209
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid logging full event content (PII leakage risk)

Debug log echoes event titles, descriptions, and locations into logs. Remove or redact to prevent PII leakage.

-        log.debug("Calendar Event Content for reference: {} is: {}", ref.getReference(), sb.toString());
+        log.debug("Indexed calendar event content for reference: {}", ref.getReference());
🤖 Prompt for AI Agents
In
calendar/calendar-impl/impl/src/java/org/sakaiproject/calendar/impl/CalendarContentProducer.java
around lines 207 to 209, the debug log currently emits the full event content
(title, description, location) which risks PII leakage; change the log to avoid
printing the full sb.toString() by either removing the content from the message
or redacting sensitive fields — e.g., log only the event reference/id and a
non-sensitive indicator such as content length or a hash, or replace
title/description/location with masked values; ensure any remaining debug output
contains no user-identifiable text.


@Override
public Reader getContentReader(String reference) {
return new StringReader(getContent(reference));
}

@Override
public String getId(String ref) {
try {
Reference reference = getReference(ref);
if (reference != null) {
return reference.getId();
}
} catch (Exception e) {
log.debug("Error getting id for reference: {}", ref, e);
}
return "";
}

public List<String> getSiteContent(String context) {
List<String> rv = new ArrayList<>();

try {
// Get the calendar for this site
String calendarReference = calendarService.calendarReference(context, SiteService.MAIN_CONTAINER);
Calendar calendar = calendarService.getCalendar(calendarReference);

if (calendar != null && calendar.allowGetEvents()) {
// Get all events from the calendar
List<CalendarEvent> events = calendar.getEvents(null, null);
for (CalendarEvent event : events) {
rv.add(event.getReference());
}
}
} catch (Exception e) {
log.warn("Error getting site content for context: {}", context, e);
}

return rv;
}

@Override
public Iterator<String> getSiteContentIterator(String context) {
return getSiteContent(context).iterator();
}

@Override
public String getSiteId(String reference) {
Reference ref = getReference(reference);
if (ref == null) return null;

CalendarEvent event = getCalendarEvent(ref);
if (event == null) return null;

return event.getSiteId();
}

@Override
public String getSubType(String ref) {
return "";
}

@Override
public String getTitle(String reference) {
Reference ref = getReference(reference);
if (ref == null) return "";

CalendarEvent event = getCalendarEvent(ref);
if (event == null) return "";

String displayName = event.getDisplayName();
return displayName != null ? displayName : "";
}

@Override
public String getTool() {
return "calendar";
}

@Override
public String getType(String ref) {
return "calendar";
}

@Override
public String getUrl(String reference) {
Reference ref = getReference(reference);
if (ref == null) return "";

CalendarEvent event = getCalendarEvent(ref);
if (event == null) return "";

String siteId = event.getSiteId();
// Use directtool URL for better linking to specific events
try {
// Find the calendar tool in the site
Site site = siteService.getSite(siteId);
ToolConfiguration toolConfig = site.getToolForCommonId("sakai.schedule");
if (toolConfig != null) {
return "/portal/directtool/" + toolConfig.getId() +
"?eventReference=" + reference +
"&panel=Main&sakai_action=doDescription&sakai.state.reset=true";
}
} catch (Exception e) {
log.debug("Error getting tool configuration for site: {}", siteId, e);
}

// Fallback to regular site URL
return "/portal/site/" + siteId + "/tool/sakai.schedule?eventReference=" + reference +
"&panel=Main&sakai_action=doDescription&sakai.state.reset=true";
}
Comment on lines +309 to +320
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

URL-encode query parameters to avoid broken links and injection issues

reference can contain reserved characters. Encode it for both directtool and site URLs.

-                return "/portal/directtool/" + toolConfig.getId() + 
-                       "?eventReference=" + reference + 
+                String encodedRef = java.net.URLEncoder.encode(reference, java.nio.charset.StandardCharsets.UTF_8.name());
+                return "/portal/directtool/" + toolConfig.getId() +
+                       "?eventReference=" + encodedRef +
                        "&panel=Main&sakai_action=doDescription&sakai.state.reset=true";
...
-        return "/portal/site/" + siteId + "/tool/sakai.schedule?eventReference=" + reference + 
+        String encodedRef = java.net.URLEncoder.encode(reference, java.nio.charset.StandardCharsets.UTF_8.name());
+        return "/portal/site/" + siteId + "/tool/sakai.schedule?eventReference=" + encodedRef +
                "&panel=Main&sakai_action=doDescription&sakai.state.reset=true";

Add imports if you prefer to pre-import:

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
🤖 Prompt for AI Agents
In
calendar/calendar-impl/impl/src/java/org/sakaiproject/calendar/impl/CalendarContentProducer.java
around lines 309 to 320, the reference query parameter is inserted raw into two
returned URLs which can break links or allow injection; URL-encode reference
before concatenation (e.g. use URLEncoder.encode(reference,
StandardCharsets.UTF_8.name())) for both the "/portal/directtool/..." and
"/portal/site/..." return paths, add imports for java.net.URLEncoder and
java.nio.charset.StandardCharsets, and handle the checked exception or let it
propagate appropriately (wrap encoding in a try/catch and log/throw if encoding
fails).


@Override
public boolean isContentFromReader(String reference) {
return false;
}

@Override
public boolean isForIndex(String reference) {
Reference ref = getReference(reference);
if (ref == null) return false;

CalendarEvent event = getCalendarEvent(ref);
if (event == null) return false;

// Only index events that are accessible (this will be checked again in canRead)
return true;
}

@Override
public boolean matches(String reference) {
return reference.startsWith(CalendarService.REFERENCE_ROOT);
}

@Override
public boolean matches(Event event) {
return matches(event.getResource());
}

/**
* Helper method to get CalendarEvent from a reference
*/
private CalendarEvent getCalendarEvent(Reference ref) {
try {
EntityProducer ep = getProducer(ref);
if (ep instanceof CalendarService) {
CalendarService calService = (CalendarService) ep;
return (CalendarEvent) calService.getEntity(ref);
}
} catch (Exception e) {
log.debug("Error getting CalendarEvent for reference: {}", ref.getReference(), e);
}
return null;
}
}
Loading
Loading