From 3e8798ebc32bddb3087c4525d7b1f7682ac7b20f Mon Sep 17 00:00:00 2001 From: Vagisha Sharma Date: Fri, 11 Sep 2026 23:40:25 -0700 Subject: [PATCH] Bound the project search parameters instead of concatenating them * ProjectsSearcher built its WHERE clause by string concatenation, so a search word containing an apostrophe closed the literal. Searching for Parkinson's threw a syntax error on the live site, and the error page returned the query fragment * Search words, project types and both dates now go through bind parameters * The ten columns a search word is matched against were written out twice. They are now one array and one tokenConstraint helper * setEndDate built its date without separators, giving 2026911 rather than 2026-9-11. Nothing calls it, so nothing was broken, but the hand-built string is gone * SearchProjectsAction and SortProjectSearchAction now require the administrators group. The link is on the ADMIN menu but neither action checked, so any logged-in user could reach the search by posting to it directly Co-Authored-By: Claude --- src/org/yeastrc/project/ProjectsSearcher.java | 133 +++++++++--------- .../www/project/SearchProjectsAction.java | 10 ++ .../www/project/SortProjectSearchAction.java | 9 ++ 3 files changed, 85 insertions(+), 67 deletions(-) diff --git a/src/org/yeastrc/project/ProjectsSearcher.java b/src/org/yeastrc/project/ProjectsSearcher.java index f3743c90..d49b6b27 100644 --- a/src/org/yeastrc/project/ProjectsSearcher.java +++ b/src/org/yeastrc/project/ProjectsSearcher.java @@ -43,6 +43,9 @@ public List search() throws SQLException { try { boolean haveConstraint = false; + // One entry per ? in sqlStr, in the order the placeholders appear. + List params = new ArrayList(); + String sqlStr = "SELECT DISTINCT P.projectID, P.projectSubmitDate "; sqlStr += "FROM tblProjects AS P "; sqlStr += "LEFT OUTER JOIN projectResearcher AS PR ON P.projectID = PR.projectID "; @@ -53,22 +56,22 @@ public List search() throws SQLException { if (this.types.size() > 0) { sqlStr += " WHERE"; haveConstraint = true; - + sqlStr += " ("; - + Iterator iter = this.types.iterator(); - String type = iter.next(); - sqlStr += "P.projectType = '" + type + "'"; - + sqlStr += "P.projectType = ?"; + params.add(iter.next()); + while (iter.hasNext()) { - type = iter.next(); - sqlStr += " OR P.projectType = '" + type + "'"; + sqlStr += " OR P.projectType = ?"; + params.add(iter.next()); } - + sqlStr += ")"; } - // We have search tokens + // We have search tokens. A project has to match every one of them. if (this.searchTokens.size() > 0) { if (haveConstraint) { sqlStr += " AND"; @@ -76,57 +79,19 @@ public List search() throws SQLException { sqlStr += " WHERE"; haveConstraint = true; } - + Iterator iter = this.searchTokens.iterator(); - String tok = iter.next(); - - String tokSearchStr = "(RPI.researcherLastName LIKE '%" + tok + "%' OR "; - tokSearchStr += "RPI.researcherFirstName LIKE '%" + tok + "%' OR "; - tokSearchStr += "R.researcherLastName LIKE '%" + tok + "%' OR "; - tokSearchStr += "R.researcherFirstName LIKE '%" + tok + "%' OR "; - try - { - int projectId = Integer.parseInt(tok); - tokSearchStr += "P.projectID = " + projectId + " OR "; - } - catch(NumberFormatException ignored){} - tokSearchStr += "P.projectAbstract LIKE '%" + tok + "%' OR "; - tokSearchStr += "P.publicAbstract LIKE '%" + tok + "%' OR "; - tokSearchStr += "P.projectKeywords LIKE '%" + tok + "%' OR "; - tokSearchStr += "P.projectProgress LIKE '%" + tok + "%' OR "; - tokSearchStr += "P.scientificQuestion LIKE '%" + tok + "%' OR "; - tokSearchStr += "P.projectTitle LIKE '%" + tok + "%')"; - - sqlStr += " ("; - - sqlStr += tokSearchStr; - + sqlStr += " ("; + sqlStr += tokenConstraint(iter.next(), params); + while (iter.hasNext()) { - tok = iter.next(); - tokSearchStr = "(RPI.researcherLastName LIKE '%" + tok + "%' OR "; - tokSearchStr += "RPI.researcherFirstName LIKE '%" + tok + "%' OR "; - tokSearchStr += "R.researcherLastName LIKE '%" + tok + "%' OR "; - tokSearchStr += "R.researcherFirstName LIKE '%" + tok + "%' OR "; - try - { - int projectId = Integer.parseInt(tok); - tokSearchStr += "P.projectID = " + projectId + " OR "; - } - catch(NumberFormatException ignored){} - tokSearchStr += "P.projectAbstract LIKE '%" + tok + "%' OR "; - tokSearchStr += "P.publicAbstract LIKE '%" + tok + "%' OR "; - tokSearchStr += "P.projectKeywords LIKE '%" + tok + "%' OR "; - tokSearchStr += "P.projectProgress LIKE '%" + tok + "%' OR "; - tokSearchStr += "P.scientificQuestion LIKE '%" + tok + "%' OR "; - tokSearchStr += "P.projectTitle LIKE '%" + tok + "%')"; - - sqlStr += " AND " + tokSearchStr; + sqlStr += " AND " + tokenConstraint(iter.next(), params); } - + sqlStr += ")"; } - + // Start date constraint if (this.startDate != null) { if (haveConstraint) { sqlStr += " AND"; } @@ -134,12 +99,9 @@ public List search() throws SQLException { sqlStr += " WHERE"; haveConstraint = true; } - - String year = String.valueOf(this.startDate.getYear() + 1900); - String month = String.valueOf(this.startDate.getMonth() + 1); - String day = String.valueOf(this.startDate.getDate()); - - sqlStr += " P.projectSubmitDate >= '" + year + "-" + month + "-" + day + "'"; + + sqlStr += " P.projectSubmitDate >= ?"; + params.add(new java.sql.Date(this.startDate.getTime())); } // End date constraint @@ -149,12 +111,9 @@ public List search() throws SQLException { sqlStr += " WHERE"; haveConstraint = true; } - - String year = String.valueOf(this.endDate.getYear() + 1900); - String month = String.valueOf(this.endDate.getMonth() + 1); - String day = String.valueOf(this.endDate.getDate()); - - sqlStr += " P.projectSubmitDate <= '" + year + month + day + "'"; + + sqlStr += " P.projectSubmitDate <= ?"; + params.add(new java.sql.Date(this.endDate.getTime())); } // Archived constraint @@ -169,8 +128,11 @@ public List search() throws SQLException { } sqlStr += " ORDER BY P.projectSubmitDate"; - + stmt = conn.prepareStatement(sqlStr); + for (int i = 0; i < params.size(); i++) { + stmt.setObject(i + 1, params.get(i)); + } rs = stmt.executeQuery(); while (rs.next()) { @@ -252,6 +214,43 @@ public List search() throws SQLException { return retList; } + /** + * The columns one search word is matched against. + */ + private static final String[] TOKEN_COLUMNS = { + "RPI.researcherLastName", "RPI.researcherFirstName", + "R.researcherLastName", "R.researcherFirstName", + "P.projectAbstract", "P.publicAbstract", "P.projectKeywords", + "P.projectProgress", "P.scientificQuestion", "P.projectTitle" + }; + + /** + * A parenthesised OR over TOKEN_COLUMNS for one search word. A word that parses as an + * integer also matches that project ID. + * + * Adds one entry to params for each ? it returns, in the same order. + */ + private String tokenConstraint(String tok, List params) { + + StringBuilder constraint = new StringBuilder("("); + + for (int i = 0; i < TOKEN_COLUMNS.length; i++) { + if (i > 0) { constraint.append(" OR "); } + constraint.append(TOKEN_COLUMNS[i]).append(" LIKE ?"); + params.add("%" + tok + "%"); + } + + try { + int projectId = Integer.parseInt(tok); + constraint.append(" OR P.projectID = ?"); + params.add(projectId); + } + catch (NumberFormatException ignored) {} + + constraint.append(")"); + return constraint.toString(); + } + /** * Add a new word to use for searching * @param phrase The phrase to add diff --git a/src/org/yeastrc/www/project/SearchProjectsAction.java b/src/org/yeastrc/www/project/SearchProjectsAction.java index 5734b0cf..69b7062b 100644 --- a/src/org/yeastrc/www/project/SearchProjectsAction.java +++ b/src/org/yeastrc/www/project/SearchProjectsAction.java @@ -28,6 +28,7 @@ import org.yeastrc.project.ProjectReviewerDAO; import org.yeastrc.project.ProjectsSearcher; import org.yeastrc.project.Researcher; +import org.yeastrc.www.user.Groups; import org.yeastrc.www.user.User; import org.yeastrc.www.user.UserUtils; /** @@ -61,6 +62,15 @@ public ActionForward execute( ActionMapping mapping, return mapping.findForward("authenticate"); } + // The link to this page is on the ADMIN menu only, so the action restricts it too. + Groups groupMan = Groups.getInstance(); + if (!groupMan.isMember(user.getResearcher().getID(), "administrators")) { + ActionErrors errors = new ActionErrors(); + errors.add("access", new ActionMessage("error.access.invalidgroup")); + saveErrors( request, errors ); + return mapping.findForward("standardHome"); + } + // The Researcher Researcher researcher = user.getResearcher(); diff --git a/src/org/yeastrc/www/project/SortProjectSearchAction.java b/src/org/yeastrc/www/project/SortProjectSearchAction.java index 471f8c9b..b49dd7ae 100644 --- a/src/org/yeastrc/www/project/SortProjectSearchAction.java +++ b/src/org/yeastrc/www/project/SortProjectSearchAction.java @@ -37,6 +37,15 @@ public ActionForward execute( ActionMapping mapping, return mapping.findForward("authenticate"); } + // The link to this page is on the ADMIN menu only, so the action restricts it too. + Groups groupMan = Groups.getInstance(); + if (!groupMan.isMember(user.getResearcher().getID(), "administrators")) { + ActionErrors errors = new ActionErrors(); + errors.add("access", new ActionMessage("error.access.invalidgroup")); + saveErrors( request, errors ); + return mapping.findForward("standardHome"); + } + // Make sure we have the pre-existing list of projects from their previous search // We can have two types of projects: Subsidized projects and Billed projects