Browse Source

Restrict options in not-context and hopefully make it easier to use

microsub
J. King 5 years ago
parent
commit
b950ac066f
  1. 101
      lib/Context/Context.php
  2. 102
      lib/Context/ExclusionContext.php
  3. 28
      lib/Database.php
  4. 26
      lib/Misc/Query.php
  5. 2
      lib/REST/NextCloudNews/V1_2.php
  6. 2
      lib/REST/TinyTinyRSS/API.php
  7. 2
      tests/cases/Database/SeriesArticle.php
  8. 2
      tests/cases/Database/SeriesLabel.php
  9. 13
      tests/cases/Misc/TestContext.php
  10. 2
      tests/cases/REST/NextCloudNews/TestV1_2.php
  11. 2
      tests/cases/REST/TinyTinyRSS/TestAPI.php

101
lib/Context/Context.php

@ -0,0 +1,101 @@
<?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\Context;
use JKingWeb\Arsse\Misc\Date;
class Context extends ExclusionContext {
public $not;
public $reverse = false;
public $limit = 0;
public $offset = 0;
public $unread;
public $starred;
public $labelled;
public $annotated;
public $oldestArticle;
public $latestArticle;
public $oldestEdition;
public $latestEdition;
public $modifiedSince;
public $notModifiedSince;
public $markedSince;
public $notMarkedSince;
public function __construct() {
$this->not = new ExclusionContext;
}
public function __clone() {
// clone the exclusion context as well
$this->not = clone $this->not;
}
public function reverse(bool $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function limit(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function offset(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function unread(bool $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function starred(bool $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function labelled(bool $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function annotated(bool $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function latestArticle(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function oldestArticle(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function latestEdition(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function oldestEdition(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function modifiedSince($spec = null) {
$spec = Date::normalize($spec);
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function notModifiedSince($spec = null) {
$spec = Date::normalize($spec);
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function markedSince($spec = null) {
$spec = Date::normalize($spec);
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function notMarkedSince($spec = null) {
$spec = Date::normalize($spec);
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
}

102
lib/Misc/Context.php → lib/Context/ExclusionContext.php

@ -4,49 +4,27 @@
* See LICENSE and AUTHORS files for details */ * See LICENSE and AUTHORS files for details */
declare(strict_types=1); declare(strict_types=1);
namespace JKingWeb\Arsse\Misc; namespace JKingWeb\Arsse\Context;
use JKingWeb\Arsse\Misc\Date;
use JKingWeb\Arsse\Misc\ValueInfo; use JKingWeb\Arsse\Misc\ValueInfo;
class Context { class ExclusionContext {
public $not = null;
public $reverse = false;
public $limit = 0;
public $offset = 0;
public $folder; public $folder;
public $folderShallow; public $folderShallow;
public $subscription; public $subscription;
public $oldestArticle;
public $latestArticle;
public $oldestEdition;
public $latestEdition;
public $unread = null;
public $starred = null;
public $modifiedSince;
public $notModifiedSince;
public $markedSince;
public $notMarkedSince;
public $edition; public $edition;
public $article; public $article;
public $editions; public $editions;
public $articles; public $articles;
public $label; public $label;
public $labelName; public $labelName;
public $labelled = null; public $annotationTerms;
public $annotated = null; public $searchTerms;
public $annotationTerms = null; public $titleTerms;
public $searchTerms = null; public $authorTerms;
public $titleTerms = null;
public $authorTerms = null;
protected $props = []; protected $props = [];
public function __clone() {
// clone the negation context, if any
$this->not = $this->not ? clone $this->not : null;
}
protected function act(string $prop, int $set, $value) { protected function act(string $prop, int $set, $value) {
if ($set) { if ($set) {
if (is_null($value)) { if (is_null($value)) {
@ -87,18 +65,6 @@ class Context {
return array_values($spec); return array_values($spec);
} }
public function reverse(bool $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function limit(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function offset(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function folder(int $spec = null) { public function folder(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec); return $this->act(__FUNCTION__, func_num_args(), $spec);
} }
@ -111,50 +77,6 @@ class Context {
return $this->act(__FUNCTION__, func_num_args(), $spec); return $this->act(__FUNCTION__, func_num_args(), $spec);
} }
public function latestArticle(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function oldestArticle(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function latestEdition(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function oldestEdition(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function unread(bool $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function starred(bool $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function modifiedSince($spec = null) {
$spec = Date::normalize($spec);
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function notModifiedSince($spec = null) {
$spec = Date::normalize($spec);
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function markedSince($spec = null) {
$spec = Date::normalize($spec);
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function notMarkedSince($spec = null) {
$spec = Date::normalize($spec);
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function edition(int $spec = null) { public function edition(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec); return $this->act(__FUNCTION__, func_num_args(), $spec);
} }
@ -185,14 +107,6 @@ class Context {
return $this->act(__FUNCTION__, func_num_args(), $spec); return $this->act(__FUNCTION__, func_num_args(), $spec);
} }
public function labelled(bool $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function annotated(bool $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function annotationTerms(array $spec = null) { public function annotationTerms(array $spec = null) {
if (isset($spec)) { if (isset($spec)) {
$spec = $this->cleanStringArray($spec); $spec = $this->cleanStringArray($spec);
@ -220,8 +134,4 @@ class Context {
} }
return $this->act(__FUNCTION__, func_num_args(), $spec); return $this->act(__FUNCTION__, func_num_args(), $spec);
} }
public function not(self $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
} }

28
lib/Database.php

@ -9,7 +9,8 @@ namespace JKingWeb\Arsse;
use JKingWeb\DrUUID\UUID; use JKingWeb\DrUUID\UUID;
use JKingWeb\Arsse\Db\Statement; use JKingWeb\Arsse\Db\Statement;
use JKingWeb\Arsse\Misc\Query; use JKingWeb\Arsse\Misc\Query;
use JKingWeb\Arsse\Misc\Context; use JKingWeb\Arsse\Context\Context;
use JKingWeb\Arsse\Context\ExclusionContext;
use JKingWeb\Arsse\Misc\Date; use JKingWeb\Arsse\Misc\Date;
use JKingWeb\Arsse\Misc\ValueInfo; use JKingWeb\Arsse\Misc\ValueInfo;
@ -1178,8 +1179,9 @@ class Database {
// if there are no output columns requested we're getting a count and should not group, but otherwise we should // if there are no output columns requested we're getting a count and should not group, but otherwise we should
$q->setGroup("arsse_articles.id", "arsse_marks.note", "arsse_enclosures.url", "arsse_enclosures.type", "arsse_subscriptions.title", "arsse_feeds.title", "arsse_subscriptions.id", "arsse_marks.modified", "arsse_label_members.modified", "arsse_marks.read", "arsse_marks.starred", "latest_editions.edition"); $q->setGroup("arsse_articles.id", "arsse_marks.note", "arsse_enclosures.url", "arsse_enclosures.type", "arsse_subscriptions.title", "arsse_feeds.title", "arsse_subscriptions.id", "arsse_marks.modified", "arsse_label_members.modified", "arsse_marks.read", "arsse_marks.starred", "latest_editions.edition");
} }
$excContext = new ExclusionContext;
// handle the simple context options // handle the simple context options
foreach ([ $options = [
// each context array consists of a column identifier (see $colDefs above), a comparison operator, a data type, and an upper bound if the value is an array // each context array consists of a column identifier (see $colDefs above), a comparison operator, a data type, and an upper bound if the value is an array
"edition" => ["edition", "=", "int", 1], "edition" => ["edition", "=", "int", 1],
"editions" => ["edition", "in", "int", self::LIMIT_ARTICLES], "editions" => ["edition", "in", "int", self::LIMIT_ARTICLES],
@ -1197,7 +1199,8 @@ class Database {
"subscription" => ["subscription", "=", "int", 1], "subscription" => ["subscription", "=", "int", 1],
"unread" => ["unread", "=", "bool", 1], "unread" => ["unread", "=", "bool", 1],
"starred" => ["starred", "=", "bool", 1], "starred" => ["starred", "=", "bool", 1],
] as $m => list($col, $op, $type, $max)) { ];
foreach ($options as $m => list($col, $op, $type, $max)) {
if (!$context->$m()) { if (!$context->$m()) {
// context is not being used // context is not being used
continue; continue;
@ -1213,6 +1216,25 @@ class Database {
$q->setWhere("{$colDefs[$col]} $op ?", $type, $context->$m); $q->setWhere("{$colDefs[$col]} $op ?", $type, $context->$m);
} }
} }
if ($context->not != $excContext) {
// further handle exclusionary options if specified
foreach ($options as $m => list($col, $op, $type, $max)) {
if (!method_exists($context->not, $m) || !$context->not->$m()) {
// context option is not being used
continue;
} elseif (is_array($context->not->$m)) {
if (!$context->not->$m) {
// for exclusions we don't care if the array is empty
} elseif (sizeof($context->not->$m) > $max) {
throw new Db\ExceptionInput("tooLong", ['field' => $m, 'action' => $this->caller(), 'max' => $max]); // @codeCoverageIgnore
}
list($clause, $types, $values) = $this->generateIn($context->$m, $type);
$q->setWhereNot("{$colDefs[$col]} $op ($clause)", $types, $values);
} else {
$q->setWhereNot("{$colDefs[$col]} $op ?", $type, $context->$m);
}
}
}
// handle complex context options // handle complex context options
if ($context->labelled()) { if ($context->labelled()) {
// any label (true) or no label (false) // any label (true) or no label (false)

26
lib/Misc/Query.php

@ -20,6 +20,9 @@ class Query {
protected $qWhere = []; // WHERE clause components protected $qWhere = []; // WHERE clause components
protected $tWhere = []; // WHERE clause type bindings protected $tWhere = []; // WHERE clause type bindings
protected $vWhere = []; // WHERE clause binding values protected $vWhere = []; // WHERE clause binding values
protected $qWhereNot = []; // WHERE NOT clause components
protected $tWhereNot = []; // WHERE NOT clause type bindings
protected $vWhereNot = []; // WHERE NOT clause binding values
protected $group = []; // GROUP BY clause components protected $group = []; // GROUP BY clause components
protected $order = []; // ORDER BY clause components protected $order = []; // ORDER BY clause components
protected $limit = 0; protected $limit = 0;
@ -69,6 +72,15 @@ class Query {
return true; return true;
} }
public function setWhereNot(string $where, $types = null, $values = null): bool {
$this->qWhereNot[] = $where;
if (!is_null($types)) {
$this->tWhereNot[] = $types;
$this->vWhereNot[] = $values;
}
return true;
}
public function setGroup(string ...$column): bool { public function setGroup(string ...$column): bool {
foreach ($column as $col) { foreach ($column as $col) {
$this->group[] = $col; $this->group[] = $col;
@ -94,7 +106,7 @@ class Query {
public function pushCTE(string $tableSpec, string $join = ''): bool { public function pushCTE(string $tableSpec, string $join = ''): bool {
// this function takes the query body and converts it to a common table expression, putting it at the bottom of the existing CTE stack // this function takes the query body and converts it to a common table expression, putting it at the bottom of the existing CTE stack
// all WHERE, ORDER BY, and LIMIT parts belong to the new CTE and are removed from the main query // all WHERE, ORDER BY, and LIMIT parts belong to the new CTE and are removed from the main query
$this->setCTE($tableSpec, $this->buildQueryBody(), [$this->tBody, $this->tWhere], [$this->vBody, $this->vWhere]); $this->setCTE($tableSpec, $this->buildQueryBody(), [$this->tBody, $this->tWhere, $this->tWhereNot], [$this->vBody, $this->vWhere, $this->vWhereNot]);
$this->jCTE = []; $this->jCTE = [];
$this->tBody = []; $this->tBody = [];
$this->vBody = []; $this->vBody = [];
@ -129,11 +141,11 @@ class Query {
} }
public function getTypes(): array { public function getTypes(): array {
return [$this->tCTE, $this->tBody, $this->tJoin, $this->tWhere]; return [$this->tCTE, $this->tBody, $this->tJoin, $this->tWhere, $this->tWhereNot];
} }
public function getValues(): array { public function getValues(): array {
return [$this->vCTE, $this->vBody, $this->vJoin, $this->vWhere]; return [$this->vCTE, $this->vBody, $this->vJoin, $this->vWhere, $this->vWhereNot];
} }
public function getJoinTypes(): array { public function getJoinTypes(): array {
@ -173,8 +185,12 @@ class Query {
$out .= " ".implode(" ", $this->qJoin); $out .= " ".implode(" ", $this->qJoin);
} }
// add any WHERE terms // add any WHERE terms
if (sizeof($this->qWhere)) { if (sizeof($this->qWhere) || sizeof($this->qWhereNot)) {
$out .= " WHERE ".implode(" AND ", $this->qWhere); $where = implode(" AND ", $this->qWhere);
$whereNot = implode(" OR ", $this->qWhereNot);
$whereNot = strlen($whereNot) ? "NOT ($whereNot)" : "";
$where = implode(" AND ", array_filter([$where, $whereNot]));
$out .= " WHERE $where";
} }
// add any GROUP BY terms // add any GROUP BY terms
if (sizeof($this->group)) { if (sizeof($this->group)) {

2
lib/REST/NextCloudNews/V1_2.php

@ -10,7 +10,7 @@ use JKingWeb\Arsse\Arsse;
use JKingWeb\Arsse\Database; use JKingWeb\Arsse\Database;
use JKingWeb\Arsse\User; use JKingWeb\Arsse\User;
use JKingWeb\Arsse\Service; use JKingWeb\Arsse\Service;
use JKingWeb\Arsse\Misc\Context; use JKingWeb\Arsse\Context\Context;
use JKingWeb\Arsse\Misc\ValueInfo; use JKingWeb\Arsse\Misc\ValueInfo;
use JKingWeb\Arsse\AbstractException; use JKingWeb\Arsse\AbstractException;
use JKingWeb\Arsse\Db\ExceptionInput; use JKingWeb\Arsse\Db\ExceptionInput;

2
lib/REST/TinyTinyRSS/API.php

@ -12,7 +12,7 @@ use JKingWeb\Arsse\Database;
use JKingWeb\Arsse\User; use JKingWeb\Arsse\User;
use JKingWeb\Arsse\Service; use JKingWeb\Arsse\Service;
use JKingWeb\Arsse\Misc\Date; use JKingWeb\Arsse\Misc\Date;
use JKingWeb\Arsse\Misc\Context; use JKingWeb\Arsse\Context\Context;
use JKingWeb\Arsse\Misc\ValueInfo; use JKingWeb\Arsse\Misc\ValueInfo;
use JKingWeb\Arsse\AbstractException; use JKingWeb\Arsse\AbstractException;
use JKingWeb\Arsse\ExceptionType; use JKingWeb\Arsse\ExceptionType;

2
tests/cases/Database/SeriesArticle.php

@ -8,7 +8,7 @@ namespace JKingWeb\Arsse\TestCase\Database;
use JKingWeb\Arsse\Database; use JKingWeb\Arsse\Database;
use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\Arsse;
use JKingWeb\Arsse\Misc\Context; use JKingWeb\Arsse\Context\Context;
use JKingWeb\Arsse\Misc\Date; use JKingWeb\Arsse\Misc\Date;
use Phake; use Phake;

2
tests/cases/Database/SeriesLabel.php

@ -7,7 +7,7 @@ declare(strict_types=1);
namespace JKingWeb\Arsse\TestCase\Database; namespace JKingWeb\Arsse\TestCase\Database;
use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\Arsse;
use JKingWeb\Arsse\Misc\Context; use JKingWeb\Arsse\Context\Context;
use JKingWeb\Arsse\Misc\Date; use JKingWeb\Arsse\Misc\Date;
use Phake; use Phake;

13
tests/cases/Misc/TestContext.php

@ -6,10 +6,10 @@
declare(strict_types=1); declare(strict_types=1);
namespace JKingWeb\Arsse\TestCase\Misc; namespace JKingWeb\Arsse\TestCase\Misc;
use JKingWeb\Arsse\Misc\Context; use JKingWeb\Arsse\Context\Context;
use JKingWeb\Arsse\Misc\ValueInfo; use JKingWeb\Arsse\Misc\ValueInfo;
/** @covers \JKingWeb\Arsse\Misc\Context */ /** @covers \JKingWeb\Arsse\Context\Context<extended> */
class TestContext extends \JKingWeb\Arsse\Test\AbstractTest { class TestContext extends \JKingWeb\Arsse\Test\AbstractTest {
public function testVerifyInitialState() { public function testVerifyInitialState() {
$c = new Context; $c = new Context;
@ -96,4 +96,13 @@ class TestContext extends \JKingWeb\Arsse\Test\AbstractTest {
$this->assertSame($out, $c->$method($in)->$method, "Context method $method did not return the expected results"); $this->assertSame($out, $c->$method($in)->$method, "Context method $method did not return the expected results");
} }
} }
public function testCloneAContext() {
$c1 = new Context;
$c2 = clone $c1;
$this->assertEquals($c1, $c2);
$this->assertEquals($c1->not, $c2->not);
$this->assertNotSame($c1, $c2);
$this->assertNotSame($c1->not, $c2->not);
}
} }

2
tests/cases/REST/NextCloudNews/TestV1_2.php

@ -13,7 +13,7 @@ use JKingWeb\Arsse\Database;
use JKingWeb\Arsse\Service; use JKingWeb\Arsse\Service;
use JKingWeb\Arsse\Test\Result; use JKingWeb\Arsse\Test\Result;
use JKingWeb\Arsse\Misc\Date; use JKingWeb\Arsse\Misc\Date;
use JKingWeb\Arsse\Misc\Context; use JKingWeb\Arsse\Context\Context;
use JKingWeb\Arsse\Db\ExceptionInput; use JKingWeb\Arsse\Db\ExceptionInput;
use JKingWeb\Arsse\Db\Transaction; use JKingWeb\Arsse\Db\Transaction;
use JKingWeb\Arsse\REST\NextCloudNews\V1_2; use JKingWeb\Arsse\REST\NextCloudNews\V1_2;

2
tests/cases/REST/TinyTinyRSS/TestAPI.php

@ -14,7 +14,7 @@ use JKingWeb\Arsse\Service;
use JKingWeb\Arsse\REST\Request; use JKingWeb\Arsse\REST\Request;
use JKingWeb\Arsse\Test\Result; use JKingWeb\Arsse\Test\Result;
use JKingWeb\Arsse\Misc\Date; use JKingWeb\Arsse\Misc\Date;
use JKingWeb\Arsse\Misc\Context; use JKingWeb\Arsse\Context\Context;
use JKingWeb\Arsse\Db\ExceptionInput; use JKingWeb\Arsse\Db\ExceptionInput;
use JKingWeb\Arsse\Db\Transaction; use JKingWeb\Arsse\Db\Transaction;
use JKingWeb\Arsse\REST\TinyTinyRSS\API; use JKingWeb\Arsse\REST\TinyTinyRSS\API;

Loading…
Cancel
Save