looqs/shared/sqlitesearch.cpp
Albert S 1849eba190 shared: sqlitesearch: Escape FTS arguments
Most users are not to be expected to be familiar with
sqlite's FTS syntax. It also leads to unnnecessary
arrows in some instances.

So wrap every space separated word in quotes, unless
it's already in quotes. Then we just escape those with
double-quotes.
2022-07-28 17:49:40 +02:00

260 lines
5.7 KiB
C++

#include <QStack>
#include <QRegularExpression>
#include <QSqlQuery>
#include <QSqlError>
#include <QStringList>
#include <QDebug>
#include "sqlitesearch.h"
#include "looqsgeneralexception.h"
SqliteSearch::SqliteSearch(QSqlDatabase &db)
{
this->db = &db;
}
QString SqliteSearch::fieldToColumn(QueryField field)
{
if(field == FILE_MTIME)
{
return "file.mtime";
}
else if(field == FILE_PATH)
{
return "file.path";
}
else if(field == FILE_SIZE)
{
return "file.size";
}
else if(field == CONTENT_TEXT_PAGE)
{
return "content.page";
}
else if(field == CONTENT_TEXT)
{
return "content.text";
}
return "";
}
QString SqliteSearch::createSortSql(const QVector<SortCondition> sortConditions)
{
QString sortsql;
for(const SortCondition &sc : sortConditions)
{
QString order;
QString field = fieldToColumn(sc.field);
if(field == "")
{
throw LooqsGeneralException("Unknown sort field supplied");
}
if(sc.order == DESC)
{
order = "DESC";
}
else
{
order = "ASC";
}
sortsql += field + " " + order + ", ";
}
sortsql.chop(2);
if(!sortsql.isEmpty())
{
return " ORDER BY " + sortsql;
}
return "";
}
QString SqliteSearch::escapeFtsArgument(QString ftsArg)
{
QString result;
QRegularExpression extractor(R"#("([^"]*)"|([^\s]+))#");
QRegularExpressionMatchIterator i = extractor.globalMatch(ftsArg);
while(i.hasNext())
{
QRegularExpressionMatch m = i.next();
QString value = m.captured(1);
if(value.isEmpty())
{
value = m.captured(2);
}
else
{
value = "\"\"" + value + "\"\"";
}
result += "\"" + value + "\" ";
}
return result;
}
QPair<QString, QVector<QString>> createNonArgPair(QString key)
{
return {" " + key + " ", QVector<QString>()};
}
QPair<QString, QVector<QString>> SqliteSearch::createSql(const Token &token)
{
QPair<QString, QVector<QString>> result;
QString value = token.value;
value = value.replace("'", "\\'");
if(token.type == BOOL_AND)
{
return createNonArgPair("AND");
}
if(token.type == BOOL_OR)
{
return createNonArgPair("OR");
}
if(token.type == NEGATION)
{
return createNonArgPair("NOT");
}
if(token.type == BRACKET_OPEN)
{
return createNonArgPair("(");
}
if(token.type == BRACKET_CLOSE)
{
return createNonArgPair(")");
}
if(token.type == FILTER_PATH_STARTS)
{
return {" file.path LIKE ? || '%' ", {value}};
}
if(token.type == FILTER_PATH_ENDS)
{
return {" file.path LIKE '%' || ? ", {value}};
}
if(token.type == FILTER_PATH_CONTAINS)
{
return {" file.path LIKE '%' || ? || '%' ", {value}};
}
if(token.type == FILTER_CONTENT_PAGE)
{
return {" content.page = ?", {value}};
}
if(token.type == FILTER_CONTENT_CONTAINS)
{
return {" content.id IN (SELECT fts.ROWID FROM fts WHERE fts.content MATCH ? ORDER BY "
"rank) ",
{escapeFtsArgument(value)}};
}
throw LooqsGeneralException("Unknown token passed (should not happen)");
}
QSqlQuery SqliteSearch::makeSqlQuery(const LooqsQuery &query)
{
QString whereSql;
QString joinSql;
QVector<QString> bindValues;
bool isContentSearch = (query.getTokensMask() & FILTER_CONTENT) == FILTER_CONTENT;
if(query.getTokens().isEmpty())
{
throw LooqsGeneralException("Nothing to search for supplied");
}
bool ftsAlreadyJoined = false;
auto tokens = query.getTokens();
for(const Token &token : tokens)
{
if(token.type == FILTER_CONTENT_CONTAINS)
{
if(!ftsAlreadyJoined)
{
joinSql += " INNER JOIN fts ON content.ftsid = fts.ROWID ";
ftsAlreadyJoined = true;
}
whereSql += " fts.content MATCH ? ";
bindValues.append(escapeFtsArgument(token.value));
}
else
{
auto sql = createSql(token);
whereSql += sql.first;
bindValues.append(sql.second);
}
}
QString prepSql;
QString sortSql = createSortSql(query.getSortConditions());
if(isContentSearch)
{
if(sortSql.isEmpty())
{
if(std::find_if(tokens.begin(), tokens.end(),
[](const Token &t) -> bool { return t.type == FILTER_CONTENT_CONTAINS; }) != tokens.end())
{
sortSql = "ORDER BY rank";
}
}
prepSql =
"SELECT file.path AS path, group_concat(content.page) AS pages, file.mtime AS mtime, file.size AS size, "
"file.filetype AS filetype FROM file INNER JOIN content ON file.id = content.fileid " +
joinSql + " WHERE 1=1 AND " + whereSql + " GROUP BY file.path " + sortSql;
}
else
{
if(sortSql.isEmpty())
{
sortSql = "ORDER BY file.mtime DESC";
}
prepSql = "SELECT file.path AS path, '0' as pages, file.mtime AS mtime, file.size AS size, file.filetype AS "
"filetype FROM file WHERE 1=1 AND " +
whereSql + " " + sortSql;
}
if(query.getLimit() > 0)
{
prepSql += " LIMIT " + QString::number(query.getLimit());
}
QSqlQuery dbquery(*db);
dbquery.prepare(prepSql);
for(const QString &value : bindValues)
{
if(value != "")
{
dbquery.addBindValue(value);
}
}
return dbquery;
}
QVector<SearchResult> SqliteSearch::search(const LooqsQuery &query)
{
QVector<SearchResult> results;
QSqlQuery dbQuery = makeSqlQuery(query);
bool success = dbQuery.exec();
if(!success)
{
qDebug() << dbQuery.lastError();
qDebug() << dbQuery.executedQuery();
throw LooqsGeneralException("SQL Error: " + dbQuery.lastError().text());
}
bool contentSearch = query.hasContentSearch();
while(dbQuery.next())
{
SearchResult result;
result.fileData.absPath = dbQuery.value("path").toString();
result.fileData.mtime = dbQuery.value("mtime").toUInt();
result.fileData.size = dbQuery.value("size").toUInt();
result.fileData.filetype = dbQuery.value("filetype").toChar();
QString pages = dbQuery.value("pages").toString();
QStringList pagesList = pages.split(",");
for(QString &page : pagesList)
{
if(page != "")
{
result.pages.append(page.toUInt());
}
}
result.wasContentSearch = contentSearch;
results.append(result);
}
return results;
}