mirror of
https://code.mensbeam.com/MensBeam/Arsse.git
synced 2024-12-22 13:12:41 +00:00
Basic substring searching
This commit is contained in:
parent
570a9b171c
commit
bc3182a961
2 changed files with 57 additions and 4 deletions
|
@ -39,6 +39,8 @@ class Database {
|
|||
const SCHEMA_VERSION = 4;
|
||||
/** The maximum number of articles to mark in one query without chunking */
|
||||
const LIMIT_ARTICLES = 50;
|
||||
/** The maximum number of search terms allowed; this is a hard limit */
|
||||
const LIMIT_TERMS = 100;
|
||||
/** A map database driver short-names and their associated class names */
|
||||
const DRIVER_NAMES = [
|
||||
'sqlite3' => \JKingWeb\Arsse\Db\SQLite3\Driver::class,
|
||||
|
@ -149,6 +151,35 @@ class Database {
|
|||
return $out;
|
||||
}
|
||||
|
||||
/** Computes basic LIKE-based text search constraints for use in a WHERE clause
|
||||
*
|
||||
* Returns an indexed array containing the clause text, an array of types, and another array of values
|
||||
*
|
||||
* The clause is structured such that all terms must be present across any of the columns
|
||||
*
|
||||
* @param string[] $terms The terms to search for
|
||||
* @param string[] $cols The columns to match against; these are -not- sanitized, so much -not- come directly from user input
|
||||
*/
|
||||
protected function generateSearch(array $terms, array $cols): array {
|
||||
$clause = [];
|
||||
$types = [];
|
||||
$values = [];
|
||||
$like = $this->db->sqlToken("like");
|
||||
foreach($terms as $term) {
|
||||
$term = str_replace(["%", "_", "^"], ["^%", "^_", "^^"], $term);
|
||||
$term = "%$term%";
|
||||
$spec = [];
|
||||
foreach ($cols as $col) {
|
||||
$spec[] = "$col $like ? escape '^'";
|
||||
$types[] = "str";
|
||||
$values[] = $term;
|
||||
}
|
||||
$clause[] = "(".implode(" or ", $spec).")";
|
||||
}
|
||||
$clause = "(".implode(" and ", $clause).")";
|
||||
return [$clause, $types, $values];
|
||||
}
|
||||
|
||||
/** Returns a Transaction object, which is rolled back unless explicitly committed */
|
||||
public function begin(): Db\Transaction {
|
||||
return $this->db->begin();
|
||||
|
@ -1160,7 +1191,7 @@ class Database {
|
|||
list($inParams, $inTypes) = $this->generateIn($context->editions, "int");
|
||||
$q->setWhere("latest_editions.edition in ($inParams)", $inTypes, $context->editions);
|
||||
} elseif ($context->articles()) {
|
||||
// if multiple specific articles have been requested, prepare a CTE to list them and their articles
|
||||
// if multiple specific articles have been requested, filter against the list
|
||||
if (!$context->articles) {
|
||||
throw new Db\ExceptionInput("tooShort", ['field' => "articles", 'action' => __FUNCTION__, 'min' => 1]); // must have at least one array element
|
||||
} elseif (sizeof($context->articles) > self::LIMIT_ARTICLES) {
|
||||
|
@ -1221,6 +1252,15 @@ class Database {
|
|||
$comp = ($context->annotated) ? "<>" : "=";
|
||||
$q->setWhere("coalesce(arsse_marks.note,'') $comp ''");
|
||||
}
|
||||
// filter based on search terms
|
||||
if ($context->searchTerms()) {
|
||||
if (!$context->searchTerms) {
|
||||
throw new Db\ExceptionInput("tooShort", ['field' => "searchTerms", 'action' => __FUNCTION__, 'min' => 1]); // must have at least one array element
|
||||
} elseif (sizeof($context->searchTerms) > self::LIMIT_TERMS) {
|
||||
throw new Db\ExceptionInput("tooLong", ['field' => "searchTerms", 'action' => __FUNCTION__, 'max' => self::LIMIT_TERMS]);
|
||||
}
|
||||
$q->setWhere(...$this->generateSearch($context->searchTerms, ["arsse_articles.title", "arsse_articles.content"]));
|
||||
}
|
||||
// return the query
|
||||
return $q;
|
||||
}
|
||||
|
|
|
@ -111,9 +111,9 @@ trait SeriesArticle {
|
|||
'modified' => "datetime",
|
||||
],
|
||||
'rows' => [
|
||||
[1,1,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
|
||||
[2,1,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
|
||||
[3,2,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
|
||||
[1,1,null,"Title one", null,null,null,"First article", null,"","","","2000-01-01T00:00:00Z"],
|
||||
[2,1,null,"Title two", null,null,null,"Second article",null,"","","","2010-01-01T00:00:00Z"],
|
||||
[3,2,null,"Title three",null,null,null,"Third article", null,"","","","2000-01-01T00:00:00Z"],
|
||||
[4,2,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
|
||||
[5,3,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
|
||||
[6,3,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
|
||||
|
@ -494,6 +494,9 @@ trait SeriesArticle {
|
|||
// get specific starred articles
|
||||
$compareIds([1], (new Context)->articles([1,2,3])->starred(true));
|
||||
$compareIds([2,3], (new Context)->articles([1,2,3])->starred(false));
|
||||
// get items that match search terms
|
||||
$compareIds([1,2,3], (new Context)->searchTerms(["Article"]));
|
||||
$compareIds([1], (new Context)->searchTerms(["one", "first"]));
|
||||
}
|
||||
|
||||
public function testListArticlesOfAMissingFolder() {
|
||||
|
@ -985,4 +988,14 @@ trait SeriesArticle {
|
|||
$this->assertException("notAuthorized", "User", "ExceptionAuthz");
|
||||
Arsse::$db->articleCategoriesGet($this->user, 19);
|
||||
}
|
||||
|
||||
public function testSearchTooFewTerms() {
|
||||
$this->assertException("tooShort", "Db", "ExceptionInput");
|
||||
Arsse::$db->articleList($this->user, (new Context)->searchTerms([]));
|
||||
}
|
||||
|
||||
public function testSearchTooManyTerms() {
|
||||
$this->assertException("tooLong", "Db", "ExceptionInput");
|
||||
Arsse::$db->articleList($this->user, (new Context)->searchTerms(range(1, 105)));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue