/* * See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.xwiki.query.internal; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import org.xwiki.query.Query; import org.xwiki.query.WrappingQuery; import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; import net.sf.jsqlparser.expression.JdbcNamedParameter; import net.sf.jsqlparser.expression.JdbcParameter; import net.sf.jsqlparser.expression.operators.relational.LikeExpression; import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.statement.select.PlainSelect; import net.sf.jsqlparser.statement.select.Select; import net.sf.jsqlparser.statement.select.SelectBody; /** * Wraps a {@link Query} to perform modifications on it in order to modify the parameters and statements to change * the default escape character and escape parameters. See {@link EscapeLikeParametersFilter} for more details. * * @version $Id: 947b1fc0b3df9773f31b429b2dca9b44dbefb154 $ * @since 8.4.5 * @since 9.3RC1 */ public class EscapeLikeParametersQuery extends WrappingQuery { private List<String> modifiedNamedParameters; private List<Integer> modifiedPositionalParameters; /** * @param wrappedQuery the query to wrap and for which we're changing some behavior */ public EscapeLikeParametersQuery(Query wrappedQuery) { super(wrappedQuery); } @Override public Map<String, Object> getNamedParameters() { return convertParameters(modifiedNamedParameters, super.getNamedParameters()); } @Override public Map<Integer, Object> getPositionalParameters() { return convertParameters(modifiedPositionalParameters, super.getPositionalParameters()); } private <T> Map<T, Object> convertParameters(List<T> modifiedParameters, Map<T, Object> parametersToEscape) { if (modifiedParameters != null) { // Escape entries from the Map where needed Map<T, Object> escapedMap = new LinkedHashMap<>(); for (Map.Entry<T, Object> entry : parametersToEscape.entrySet()) { // TODO: Also handle Arrays and collections in the future if (modifiedParameters.contains(entry.getKey()) && entry.getValue() instanceof DefaultQueryParameter) { // Join the parameter parts and escape the literal parts DefaultQueryParameter queryParameter = (DefaultQueryParameter) entry.getValue(); StringBuffer buffer = new StringBuffer(); for (ParameterPart part : queryParameter.getParts()) { if (part instanceof LiteralParameterPart) { // SQL92 only specifies "%", "_" and the escape character itself. // However some DBs also support "[specifier]" and "[^specifier]" so by escaping "[" we're // playing it safe. See http://bit.ly/2ongxm6 // Now the problem is that most databases don't accept escaping any character and they only // support escaping the special characters such as "%", "_" and the escape character itself. // See https://jira.xwiki.org/browse/XWIKI-14217 and // https://groups.google.com/d/msg/h2-database/jT0O3rNgpSw/hU_JKXRkZNoJ // Thus we don't escape '[' FTM meaning that the query could fail on Sybase and SQL Server buffer.append(part.getValue().replaceAll("([%_!])", "!$1")); } else { buffer.append(part.getValue()); } } escapedMap.put(entry.getKey(), buffer.toString()); } else { escapedMap.put(entry.getKey(), entry.getValue()); } } return escapedMap; } else { return parametersToEscape; } } @Override public String getStatement() { String statement; if (getLanguage().equals(Query.HQL)) { try { statement = modifyStatement(super.getStatement()); } catch (JSQLParserException e) { throw new RuntimeException(String.format("Invalid HQL query [%s]", super.getStatement()), e); } } else { statement = super.getStatement(); } return statement; } /** * Handle the case of MySQL: in MySQL a '\' character is a special escape character. In addition we often * use '\' in Entity References. For example to find nested pages in a page with a dot would result in * something like "LIKE '.%.a\.b.%'" which wouldn't work on MySQL. Thus we need to replace the default * escape character with another one. To be safe we verify that the statement doesn't already specify an ESCAPE * term. */ private String modifyStatement(String statementString) throws JSQLParserException { Statement statement = CCJSqlParserUtil.parse(statementString); if (statement instanceof Select) { Select select = (Select) statement; SelectBody selectBody = select.getSelectBody(); if (selectBody instanceof PlainSelect) { PlainSelect plainSelect = (PlainSelect) selectBody; Expression where = plainSelect.getWhere(); where.accept(new XWikiExpressionVisitor()); } } return statement.toString(); } private class XWikiExpressionVisitor extends ExpressionVisitorAdapter { @Override public void visit(LikeExpression expr) { if (expr.getEscape() == null) { expr.setEscape("!"); expr.accept(new XWikiLikeExpressionVisitor()); } } } private class XWikiLikeExpressionVisitor extends ExpressionVisitorAdapter { @Override public void visit(JdbcParameter parameter) { if (modifiedPositionalParameters == null) { modifiedPositionalParameters = new ArrayList<>(); } // Remove one to the index since we're using a JPQL parser and JPQL starts at 1 // but HQL positional parameters start a 0. modifiedPositionalParameters.add(parameter.getIndex() - 1); } @Override public void visit(JdbcNamedParameter parameter) { if (modifiedNamedParameters == null) { modifiedNamedParameters = new ArrayList<>(); } modifiedNamedParameters.add(parameter.getName()); } } }