1
1
Fork 0
mirror of https://code.mensbeam.com/MensBeam/Arsse.git synced 2024-12-23 08:14:56 +00:00
Arsse/lib/REST/TinyTinyRSS/Search.php

357 lines
16 KiB
PHP
Raw Normal View History

2019-02-28 20:31:33 +00:00
<?php
/** @license MIT
* Copyright 2017 J. King, Dustin Wilson et al.
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\REST\TinyTinyRSS;
use JKingWeb\Arsse\Context\Context;
use JKingWeb\Arsse\Misc\Date;
class Search {
2020-03-01 23:32:01 +00:00
protected const STATE_BEFORE_TOKEN = 0;
protected const STATE_BEFORE_TOKEN_QUOTED = 1;
protected const STATE_IN_DATE = 2;
protected const STATE_IN_DATE_QUOTED = 3;
protected const STATE_IN_TOKEN_OR_TAG = 4;
protected const STATE_IN_TOKEN_OR_TAG_QUOTED = 5;
protected const STATE_IN_TOKEN = 6;
protected const STATE_IN_TOKEN_QUOTED = 7;
2019-02-28 20:31:33 +00:00
2020-03-01 23:32:01 +00:00
protected const FIELDS_BOOLEAN = [
2019-02-28 20:31:33 +00:00
"unread" => "unread",
"star" => "starred",
"note" => "annotated",
"pub" => "published", // TODO: not implemented
];
2020-03-01 23:32:01 +00:00
protected const FIELDS_TEXT = [
2019-02-28 20:31:33 +00:00
"title" => "titleTerms",
"author" => "authorTerms",
"note" => "annotationTerms",
"" => "searchTerms",
];
public static function parse(string $search, string $tz, Context $context = null): ?Context {
2019-02-28 20:31:33 +00:00
// normalize the input
$search = strtolower(trim(preg_replace("<\s+>", " ", $search)));
// set initial state
$pos = -1;
$stop = strlen($search);
$state = self::STATE_BEFORE_TOKEN;
$buffer = "";
$tag = "";
$flag_negative = false;
$context = $context ?? new Context;
// process
try {
while (++$pos <= $stop) {
$char = @$search[$pos];
switch ($state) {
case self::STATE_BEFORE_TOKEN:
switch ($char) {
case "":
continue 3;
case " ":
continue 3;
case '"':
if ($flag_negative) {
$buffer .= $char;
$state = self::STATE_IN_TOKEN_OR_TAG;
} else {
$state = self::STATE_BEFORE_TOKEN_QUOTED;
}
continue 3;
case "-":
if (!$flag_negative) {
$flag_negative = true;
} else {
$buffer .= $char;
$state = self::STATE_IN_TOKEN_OR_TAG;
}
continue 3;
case "@":
$state = self::STATE_IN_DATE;
continue 3;
case ":":
$state = self::STATE_IN_TOKEN;
continue 3;
default:
$buffer .= $char;
$state = self::STATE_IN_TOKEN_OR_TAG;
continue 3;
}
2019-05-02 02:52:20 +00:00
// no break
2019-02-28 20:31:33 +00:00
case self::STATE_BEFORE_TOKEN_QUOTED:
switch ($char) {
case "":
continue 3;
case '"':
if (($pos + 1 == $stop) || $search[$pos + 1] === " ") {
$context = self::processToken($context, $buffer, $tag, $flag_negative);
2019-02-28 20:31:33 +00:00
$state = self::STATE_BEFORE_TOKEN;
$flag_negative = false;
$buffer = $tag = "";
} elseif ($search[$pos + 1] === '"') {
$buffer .= '"';
$pos++;
$state = self::STATE_IN_TOKEN_OR_TAG_QUOTED;
} else {
$state = self::STATE_IN_TOKEN_OR_TAG;
}
continue 3;
case "\\":
if ($pos + 1 == $stop) {
$buffer .= $char;
} elseif ($search[$pos + 1] === '"') {
$buffer .= '"';
$pos++;
} else {
$buffer .= $char;
}
$state = self::STATE_IN_TOKEN_OR_TAG_QUOTED;
continue 3;
case "-":
if (!$flag_negative) {
$flag_negative = true;
} else {
$buffer .= $char;
$state = self::STATE_IN_TOKEN_OR_TAG_QUOTED;
}
continue 3;
case "@":
$state = self::STATE_IN_DATE_QUOTED;
continue 3;
case ":":
$state = self::STATE_IN_TOKEN_QUOTED;
continue 3;
default:
$buffer .= $char;
$state = self::STATE_IN_TOKEN_OR_TAG_QUOTED;
continue 3;
}
2019-05-02 02:52:20 +00:00
// no break
2019-02-28 20:31:33 +00:00
case self::STATE_IN_DATE:
while ($pos < $stop && $search[$pos] !== " ") {
$buffer .= $search[$pos++];
}
$context = self::processToken($context, $buffer, $tag, $flag_negative, $tz);
2019-02-28 20:31:33 +00:00
$state = self::STATE_BEFORE_TOKEN;
$flag_negative = false;
$buffer = $tag = "";
continue 2;
case self::STATE_IN_DATE_QUOTED:
switch ($char) {
case "":
case '"':
if (($pos + 1 >= $stop) || $search[$pos + 1] === " ") {
$context = self::processToken($context, $buffer, $tag, $flag_negative, $tz);
2019-02-28 20:31:33 +00:00
$state = self::STATE_BEFORE_TOKEN;
$flag_negative = false;
$buffer = $tag = "";
} elseif ($search[$pos + 1] === '"') {
$buffer .= '"';
$pos++;
} else {
$state = self::STATE_IN_DATE;
}
continue 3;
case "\\":
if ($pos + 1 == $stop) {
$buffer .= $char;
} elseif ($search[$pos + 1] === '"') {
$buffer .= '"';
$pos++;
} else {
$buffer .= $char;
}
continue 3;
default:
$buffer .= $char;
continue 3;
}
2019-05-02 02:52:20 +00:00
// no break
2019-02-28 20:31:33 +00:00
case self::STATE_IN_TOKEN:
while ($pos < $stop && $search[$pos] !== " ") {
$buffer .= $search[$pos++];
}
if (!strlen($tag)) {
$buffer = ":".$buffer;
}
$context = self::processToken($context, $buffer, $tag, $flag_negative);
2019-02-28 20:31:33 +00:00
$state = self::STATE_BEFORE_TOKEN;
$flag_negative = false;
$buffer = $tag = "";
continue 2;
case self::STATE_IN_TOKEN_QUOTED:
switch ($char) {
case "":
case '"':
if (($pos + 1 >= $stop) || $search[$pos + 1] === " ") {
if (!strlen($tag)) {
$buffer = ":".$buffer;
}
$context = self::processToken($context, $buffer, $tag, $flag_negative);
2019-02-28 20:31:33 +00:00
$state = self::STATE_BEFORE_TOKEN;
$flag_negative = false;
$buffer = $tag = "";
} elseif ($search[$pos + 1] === '"') {
$buffer .= '"';
$pos++;
} else {
$state = self::STATE_IN_TOKEN;
}
continue 3;
case "\\":
if ($pos + 1 == $stop) {
$buffer .= $char;
} elseif ($search[$pos + 1] === '"') {
$buffer .= '"';
$pos++;
} else {
$buffer .= $char;
}
continue 3;
default:
$buffer .= $char;
continue 3;
}
2019-05-02 02:52:20 +00:00
// no break
2019-02-28 20:31:33 +00:00
case self::STATE_IN_TOKEN_OR_TAG:
switch ($char) {
case "":
case " ":
$context = self::processToken($context, $buffer, $tag, $flag_negative);
2019-02-28 20:31:33 +00:00
$state = self::STATE_BEFORE_TOKEN;
$flag_negative = false;
$buffer = $tag = "";
continue 3;
2019-05-02 02:52:20 +00:00
case ":":
2019-02-28 20:31:33 +00:00
$tag = $buffer;
$buffer = "";
$state = self::STATE_IN_TOKEN;
continue 3;
default:
$buffer .= $char;
continue 3;
}
2019-05-02 02:52:20 +00:00
// no break
2019-02-28 20:31:33 +00:00
case self::STATE_IN_TOKEN_OR_TAG_QUOTED:
switch ($char) {
case "":
case '"':
if (($pos + 1 >= $stop) || $search[$pos + 1] === " ") {
$context = self::processToken($context, $buffer, $tag, $flag_negative);
2019-02-28 20:31:33 +00:00
$state = self::STATE_BEFORE_TOKEN;
$flag_negative = false;
$buffer = $tag = "";
} elseif ($search[$pos + 1] === '"') {
$buffer .= '"';
$pos++;
} else {
$state = self::STATE_IN_TOKEN_OR_TAG;
}
continue 3;
case "\\":
if ($pos + 1 == $stop) {
$buffer .= $char;
} elseif ($search[$pos + 1] === '"') {
$buffer .= '"';
$pos++;
} else {
$buffer .= $char;
}
continue 3;
case ":":
$tag = $buffer;
$buffer = "";
$state = self::STATE_IN_TOKEN_QUOTED;
continue 3;
default:
$buffer .= $char;
continue 3;
}
2019-05-02 02:52:20 +00:00
// no break
2019-02-28 20:31:33 +00:00
default:
throw new \Exception; // @codeCoverageIgnore
}
}
} catch (Exception $e) {
return null;
}
return $context;
}
protected static function processToken(Context $c, string $value, string $tag, bool $neg, string $tz = null): Context {
2019-02-28 20:31:33 +00:00
if (!strlen($value) && !strlen($tag)) {
return $c;
} elseif (!strlen($value)) {
// if a tag has an empty value, the tag is treated as a search term instead
$value = "$tag:";
$tag = "";
}
if ($tz !== null) {
return self::setDate($value, $c, $neg, $tz);
2019-02-28 20:31:33 +00:00
} elseif (isset(self::FIELDS_BOOLEAN[$tag])) {
return self::setBoolean($tag, $value, $c, $neg);
} else {
return self::addTerm($tag, $value, $c, $neg);
}
}
protected static function addTerm(string $tag, string $value, Context $c, bool $neg): Context {
$c = $neg ? $c->not : $c;
$type = self::FIELDS_TEXT[$tag] ?? "";
if (!$type) {
$value = "$tag:$value";
$type = self::FIELDS_TEXT[""];
}
return $c->$type(array_merge($c->$type ?? [], [$value]));
}
protected static function setDate(string $value, Context $c, bool $neg, string $tz): Context {
2019-02-28 20:31:33 +00:00
$spec = Date::normalize($value);
// TTRSS treats invalid dates as the start of the Unix epoch; we ignore them instead
if (!$spec) {
return $c;
}
$day = $spec->format("Y-m-d");
$start = $day."T00:00:00 $tz";
$end = $day."T23:59:59 $tz";
2019-02-28 20:31:33 +00:00
$cc = $neg ? $c->not : $c;
// NOTE: TTRSS treats multiple positive dates as contradictory; we instead treat them as complimentary instead, because it makes more sense
return $cc->modifiedRanges(array_merge($cc->modifiedRanges, [[$start, $end]]));
2019-02-28 20:31:33 +00:00
}
protected static function setBoolean(string $tag, string $value, Context $c, bool $neg): Context {
$set = ["true" => true, "false" => false][$value] ?? null;
if (is_null($set)) {
return self::addTerm($tag, $value, $c, $neg);
} else {
// apply negation
$set = $neg ? !$set : $set;
if ($tag === "pub") {
// TODO: this needs to be implemented correctly if the Published feed is implemented
// currently specifying true will always yield an empty result (nothing is ever published), and specifying false is a no-op (matches everything)
if ($set) {
throw new Exception;
} else {
return $c;
}
} else {
$field = (self::FIELDS_BOOLEAN[$tag] ?? "");
if (!$c->$field()) {
// field has not yet been set; set it
return $c->$field($set);
} elseif ($c->$field == $set) {
// field is already set to same value; do nothing
return $c;
} else {
// contradiction: query would return no results
throw new Exception;
}
}
}
}
}