com.xpn.xwiki.internal.store.hibernate.query.HqlQueryUtils.java Source code

Java tutorial

Introduction

Here is the source code for com.xpn.xwiki.internal.store.hibernate.query.HqlQueryUtils.java

Source

/*
 * 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 com.xpn.xwiki.internal.store.hibernate.query;

import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.Function;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.select.FromItem;
import net.sf.jsqlparser.statement.select.Join;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import net.sf.jsqlparser.statement.select.SelectBody;
import net.sf.jsqlparser.statement.select.SelectExpressionItem;
import net.sf.jsqlparser.statement.select.SelectItem;

/**
 * Provide various SQL related utilities.
 * 
 * @version $Id: 9114a0f445ab26cf47b1d2081f91a7be515df0ab $
 * @since 7.2M2
 */
public final class HqlQueryUtils {
    private static final String DOCUMENT_FIELD_FULLNAME = "fullName";

    private static final String DOCUMENT_FIELD_NAME = "name";

    private static final String DOCUMENT_FIELD_SPACE = "space";

    private static final String DOCUMENT_FIELD_LANGUAGE = "language";

    private static final String DOCUMENT_FIELD_DEFAULTLANGUAGE = "defaultLanguage";

    private static final String DOCUMENT_FIELD_TRANSLATION = "translation";

    private static final String DOCUMENT_FIELD_HIDDEN = "hidden";

    private static final String SPACE_FIELD_REFERENCE = "reference";

    private static final String SPACE_FIELD_NAME = DOCUMENT_FIELD_NAME;

    private static final String SPACE_FIELD_PARENT = "parent";

    private static final String SPACE_FIELD_HIDDEN = DOCUMENT_FIELD_HIDDEN;

    private static final String FROM_REPLACEMENT = "$1";

    private static final Pattern FROM_DOC = Pattern.compile("com\\.xpn\\.xwiki\\.doc\\.([^ ]+)");

    private static final Pattern FROM_OBJECT = Pattern.compile("com\\.xpn\\.xwiki\\.objects\\.([^ ]+)");

    private static final Pattern FROM_RCS = Pattern.compile("com\\.xpn\\.xwiki\\.doc\\.rcs\\.([^ ]+)");

    private static final Pattern FROM_VERSION = Pattern.compile("com\\.xpn\\.xwiki\\.store\\.migration\\.([^ ]+)");

    private static final Map<String, Set<String>> ALLOWED_FIELDS;

    static {
        ALLOWED_FIELDS = new HashMap<>();

        Set<String> allowedDocFields = new HashSet<>();
        ALLOWED_FIELDS.put("XWikiDocument", allowedDocFields);
        allowedDocFields.add(DOCUMENT_FIELD_FULLNAME);
        allowedDocFields.add(DOCUMENT_FIELD_NAME);
        allowedDocFields.add(DOCUMENT_FIELD_SPACE);
        allowedDocFields.add(DOCUMENT_FIELD_LANGUAGE);
        allowedDocFields.add(DOCUMENT_FIELD_DEFAULTLANGUAGE);
        allowedDocFields.add(DOCUMENT_FIELD_TRANSLATION);
        allowedDocFields.add(DOCUMENT_FIELD_HIDDEN);

        Set<String> allowedSpaceFields = new HashSet<>();
        ALLOWED_FIELDS.put("XWikiSpace", allowedSpaceFields);
        allowedSpaceFields.add(SPACE_FIELD_REFERENCE);
        allowedSpaceFields.add(SPACE_FIELD_NAME);
        allowedSpaceFields.add(SPACE_FIELD_PARENT);
        allowedSpaceFields.add(SPACE_FIELD_HIDDEN);
    }

    private static final Logger LOGGER = LoggerFactory.getLogger(HqlQueryUtils.class);

    private HqlQueryUtils() {

    }

    /**
     * @param statement the statement to evaluate
     * @return true if the statement is complete, false otherwise
     */
    public static boolean isShortFormStatement(String statement) {
        return StringUtils.startsWithAny(statement.trim().toLowerCase(), ",", "from", "where", "order");
    }

    /**
     * @param statementString the SQL statement to check
     * @return true if the passed SQL statement is allowed
     */
    public static boolean isSafe(String statementString) {
        try {
            // TODO: should probably use a more specific Hql parser

            // FIXME: Workaround https://github.com/JSQLParser/JSqlParser/issues/163 (Support class syntax in HQL query)
            String cleanedStatement = statementString;
            cleanedStatement = FROM_DOC.matcher(cleanedStatement).replaceAll(FROM_REPLACEMENT);
            cleanedStatement = FROM_OBJECT.matcher(cleanedStatement).replaceAll(FROM_REPLACEMENT);
            cleanedStatement = FROM_RCS.matcher(cleanedStatement).replaceAll(FROM_REPLACEMENT);
            cleanedStatement = FROM_VERSION.matcher(cleanedStatement).replaceAll(FROM_REPLACEMENT);

            Statement statement = CCJSqlParserUtil.parse(cleanedStatement);

            if (statement instanceof Select) {
                Select select = (Select) statement;

                SelectBody selectBody = select.getSelectBody();

                if (selectBody instanceof PlainSelect) {
                    PlainSelect plainSelect = (PlainSelect) selectBody;

                    Map<String, String> tables = getTables(plainSelect);

                    for (SelectItem selectItem : plainSelect.getSelectItems()) {
                        if (!isSelectItemAllowed(selectItem, tables)) {
                            return false;
                        }
                    }

                    return true;
                }
            }
        } catch (JSQLParserException e) {
            // We can't parse it so lets say it's not safe
            LOGGER.warn("Failed to parse request [{}] ([{}]). Considering it not safe.", statementString,
                    ExceptionUtils.getRootCauseMessage(e));
        }

        return false;
    }

    private static Map<String, String> getTables(PlainSelect plainSelect) {
        Map<String, String> tables = new HashMap<>();

        // Add from item
        addFromItem(plainSelect.getFromItem(), tables);

        // Add joins
        List<Join> joins = plainSelect.getJoins();
        if (joins != null) {
            for (Join join : joins) {
                addFromItem(join.getRightItem(), tables);
            }
        }

        return tables;
    }

    private static void addFromItem(FromItem item, Map<String, String> tables) {
        if (item instanceof Table) {
            String tableName = ((Table) item).getName();
            tables.put(item.getAlias() != null ? item.getAlias().getName() : tableName, tableName);
        }
    }

    /**
     * @param selectItem the {@link SelectItem} to check
     * @return true if the passed {@link SelectItem} is allowed
     */
    private static boolean isSelectItemAllowed(SelectItem selectItem, Map<String, String> tables) {
        if (selectItem instanceof SelectExpressionItem) {
            SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem;

            return isSelectExpressionAllowed(selectExpressionItem.getExpression(), tables);
        }

        // TODO: we could support more select items

        return false;
    }

    private static boolean isSelectExpressionAllowed(Expression expression, Map<String, String> tables) {
        if (expression instanceof Column) {
            Column column = (Column) expression;

            if (isColumnAllowed(column, tables)) {
                return true;
            }
        } else if (expression instanceof Function) {
            Function function = (Function) expression;

            if (function.isAllColumns()) {
                // Validate that allowed table is passed to the method
                // TODO: add support for more that "count" maybe
                return function.getName().equals("count") && tables.size() == 1
                        && isTableAllowed(tables.values().iterator().next());
            } else {
                // Validate that allowed columns are used as parameters
                for (Expression parameter : function.getParameters().getExpressions()) {
                    if (!isSelectExpressionAllowed(parameter, tables)) {
                        return false;
                    }
                }

                return true;
            }
        }

        return false;
    }

    /**
     * @param column the {@link Column} to check
     * @return true if the passed {@link Column} is allowed
     */
    private static boolean isColumnAllowed(Column column, Map<String, String> tables) {
        Set<String> fields = ALLOWED_FIELDS.get(getTableName(column.getTable(), tables));
        return fields != null && fields.contains(column.getColumnName());
    }

    /**
     * @param tableName the name of the table
     * @return true if the table has at least one allowed field
     */
    private static boolean isTableAllowed(String tableName) {
        return ALLOWED_FIELDS.containsKey(tableName);
    }

    private static String getTableName(Table table, Map<String, String> tables) {
        String tableName = tables.values().iterator().next();

        if (table != null && StringUtils.isNotEmpty(table.getFullyQualifiedName())) {
            tableName = tables.get(table.getFullyQualifiedName());
        }

        return tableName;
    }
}