1
1
Fork 0
mirror of https://code.mensbeam.com/MensBeam/Arsse.git synced 2024-12-22 21:22:40 +00:00

Remove arbitrary search term limits; fixes #150

This commit is contained in:
J. King 2019-03-02 13:53:43 -05:00
parent 21fdd66d37
commit 44366f48bf
6 changed files with 25 additions and 30 deletions

View file

@ -142,7 +142,6 @@ We are not aware of any other extensions to the TTRSS protocol. If you know of a
- Providing the `setArticleLabel` operation with an invalid label normally silently fails; The Arsse returns an `INVALID_USAGE` error instead - Providing the `setArticleLabel` operation with an invalid label normally silently fails; The Arsse returns an `INVALID_USAGE` error instead
- Processing of the `search` parameter of the `getHeadlines` operation differs in the following ways: - Processing of the `search` parameter of the `getHeadlines` operation differs in the following ways:
- Values other than `"true"` or `"false"` for the `unread`, `star`, and `pub` special keywords treat the entire token as a search term rather than as `"false"` - Values other than `"true"` or `"false"` for the `unread`, `star`, and `pub` special keywords treat the entire token as a search term rather than as `"false"`
- Limits are placed on the number of search terms: ten each for `title`, `author`, and `note`, and twenty for content searching; exceeding the limits will yield a non-standard `TOO_MANY_SEARCH_TERMS` error
- Invalid dates are ignored rather than assumed to be `"1970-01-01"` - Invalid dates are ignored rather than assumed to be `"1970-01-01"`
- Only a single negative date is allowed (this is a known bug rather than intentional) - Only a single negative date is allowed (this is a known bug rather than intentional)
- Dates are always relative to UTC - Dates are always relative to UTC

View file

@ -11,6 +11,7 @@ abstract class AbstractException extends \Exception {
"Exception.uncoded" => -1, "Exception.uncoded" => -1,
"Exception.unknown" => 10000, "Exception.unknown" => 10000,
"Exception.constantUnknown" => 10001, "Exception.constantUnknown" => 10001,
"Exception.arrayEmpty" => 10002,
"ExceptionType.strictFailure" => 10011, "ExceptionType.strictFailure" => 10011,
"ExceptionType.typeUnknown" => 10012, "ExceptionType.typeUnknown" => 10012,
"Lang/Exception.defaultFileMissing" => 10101, "Lang/Exception.defaultFileMissing" => 10101,

View file

@ -185,23 +185,33 @@ class Database {
* @param boolean $matchAny Whether the search is successful when it matches any (true) or all (false) terms * @param boolean $matchAny Whether the search is successful when it matches any (true) or all (false) terms
*/ */
protected function generateSearch(array $terms, array $cols, bool $matchAny = false): array { protected function generateSearch(array $terms, array $cols, bool $matchAny = false): array {
if (!$cols) {
throw new Exception("arrayEmpty", "cols"); // @codeCoverageIgnore
}
$clause = []; $clause = [];
$types = []; $types = [];
$values = []; $values = [];
$like = $this->db->sqlToken("like"); $like = $this->db->sqlToken("like");
$embedSet = sizeof($terms) > ((int) (self::LIMIT_SET_SIZE / sizeof($cols)));
foreach($terms as $term) { foreach($terms as $term) {
$embedTerm = ($embedSet && strlen($term) <= self::LIMIT_SET_STRING_LENGTH);
$term = str_replace(["%", "_", "^"], ["^%", "^_", "^^"], $term); $term = str_replace(["%", "_", "^"], ["^%", "^_", "^^"], $term);
$term = "%$term%"; $term = "%$term%";
$term = $embedTerm ? $this->db->literalString($term) : $term;
$spec = []; $spec = [];
foreach ($cols as $col) { foreach ($cols as $col) {
if ($embedTerm) {
$spec[] = "$col $like $term escape '^'";
} else {
$spec[] = "$col $like ? escape '^'"; $spec[] = "$col $like ? escape '^'";
$types[] = "str"; $types[] = "str";
$values[] = $term; $values[] = $term;
} }
}
$clause[] = "(".implode(" or ", $spec).")"; $clause[] = "(".implode(" or ", $spec).")";
} }
$glue = $matchAny ? "or" : "and"; $glue = $matchAny ? "or" : "and";
$clause = "(".implode(" $glue ", $clause).")"; $clause = $clause ? "(".implode(" $glue ", $clause).")" : "";
return [$clause, $types, $values]; return [$clause, $types, $values];
} }
@ -1307,34 +1317,27 @@ class Database {
} }
// handle text-matching context options // handle text-matching context options
$options = [ $options = [
"titleTerms" => [10, ["arsse_articles.title"]], "titleTerms" => ["arsse_articles.title"],
"searchTerms" => [20, ["arsse_articles.title", "arsse_articles.content"]], "searchTerms" => ["arsse_articles.title", "arsse_articles.content"],
"authorTerms" => [10, ["arsse_articles.author"]], "authorTerms" => ["arsse_articles.author"],
"annotationTerms" => [20, ["arsse_marks.note"]], "annotationTerms" => ["arsse_marks.note"],
]; ];
foreach ($options as $m => list($max, $cols)) { foreach ($options as $m => $cols) {
if (!$context->$m()) { if (!$context->$m()) {
continue; continue;
} elseif (!$context->$m) { } elseif (!$context->$m) {
throw new Db\ExceptionInput("tooShort", ['field' => $m, 'action' => $this->caller(), 'min' => 1]); // must have at least one array element throw new Db\ExceptionInput("tooShort", ['field' => $m, 'action' => $this->caller(), 'min' => 1]); // must have at least one array element
} elseif (sizeof($context->$m) > $max) {
throw new Db\ExceptionInput("tooLong", ['field' => $m, 'action' => $this->caller(), 'max' => $max]);
} }
$q->setWhere(...$this->generateSearch($context->$m, $cols)); $q->setWhere(...$this->generateSearch($context->$m, $cols));
} }
// further handle exclusionary text-matching context options // further handle exclusionary text-matching context options
foreach ($options as $m => list($max, $cols)) { foreach ($options as $m => $cols) {
if (!$context->not->$m()) { if (!$context->not->$m() || !$context->not->$m) {
continue; continue;
} elseif (!$context->not->$m) {
continue;
} elseif (sizeof($context->not->$m) > $max) {
throw new Db\ExceptionInput("tooLong", ['field' => "$m (not)", 'action' => $this->caller(), 'max' => $max]);
} }
$q->setWhereNot(...$this->generateSearch($context->not->$m, $cols, true)); $q->setWhereNot(...$this->generateSearch($context->not->$m, $cols, true));
} }
// return the query // return the query
//var_export((string) $q);
return $q; return $q;
} }

View file

@ -36,6 +36,8 @@ return [
'Exception.JKingWeb/Arsse/Exception.unknown' => 'An unknown error has occurred', 'Exception.JKingWeb/Arsse/Exception.unknown' => 'An unknown error has occurred',
// indicates programming error // indicates programming error
'Exception.JKingWeb/Arsse/Exception.constantUnknown' => 'Supplied constant value ({0}) is unknown or invalid in the context in which it was used', 'Exception.JKingWeb/Arsse/Exception.constantUnknown' => 'Supplied constant value ({0}) is unknown or invalid in the context in which it was used',
// indicates programming error
'Exception.JKingWeb/Arsse/Exception.arrayEmpty' => 'Supplied array "{0}" is empty, but should have at least one element',
'Exception.JKingWeb/Arsse/ExceptionType.strictFailure' => 'Supplied value could not be normalized to {0, select, 'Exception.JKingWeb/Arsse/ExceptionType.strictFailure' => 'Supplied value could not be normalized to {0, select,
1 {null} 1 {null}
2 {boolean} 2 {boolean}

View file

@ -456,6 +456,7 @@ trait SeriesArticle {
"Excluded folder tree" => [(new Context)->not->folder(1), [1,2,3,4,19,20]], "Excluded folder tree" => [(new Context)->not->folder(1), [1,2,3,4,19,20]],
"Excluding label ID 2" => [(new Context)->not->label(2), [2,3,4,6,7,8,19]], "Excluding label ID 2" => [(new Context)->not->label(2), [2,3,4,6,7,8,19]],
"Excluding label 'Fascinating'" => [(new Context)->not->labelName("Fascinating"), [2,3,4,6,7,8,19]], "Excluding label 'Fascinating'" => [(new Context)->not->labelName("Fascinating"), [2,3,4,6,7,8,19]],
"Search 501 terms" => [(new Context)->searchTerms(array_merge(range(1,500),[str_repeat("a", 1000)])), []],
]; ];
} }
@ -991,18 +992,8 @@ trait SeriesArticle {
Arsse::$db->articleList($this->user, (new Context)->searchTerms([])); 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)));
}
public function testSearchTooFewTermsInNote() { public function testSearchTooFewTermsInNote() {
$this->assertException("tooShort", "Db", "ExceptionInput"); $this->assertException("tooShort", "Db", "ExceptionInput");
Arsse::$db->articleList($this->user, (new Context)->annotationTerms([])); Arsse::$db->articleList($this->user, (new Context)->annotationTerms([]));
} }
public function testSearchTooManyTermsInNote() {
$this->assertException("tooLong", "Db", "ExceptionInput");
Arsse::$db->articleList($this->user, (new Context)->annotationTerms(range(1, 105)));
}
} }

View file

@ -120,7 +120,6 @@ class TestSearch extends \JKingWeb\Arsse\Test\AbstractTest {
/** @dataProvider provideSearchStrings */ /** @dataProvider provideSearchStrings */
public function testApplySearchToContext(string $search, $exp) { public function testApplySearchToContext(string $search, $exp) {
$act = Search::parse($search); $act = Search::parse($search);
//var_export($act);
$this->assertEquals($exp, $act); $this->assertEquals($exp, $act);
} }
} }