Compare commits

...

78 Commits

Author SHA1 Message Date
J. King 0a8d19d37d Require PHP 7.3 6 days ago
J. King fe06ffc176 Avoid dynamic property creation with PicoFeed 6 days ago
J. King 0d6f8d2921 Avoid most deprecation warnings 1 week ago
J. King 92b1a840a1 Support PHP 8.2 properly 1 week ago
J. King a25e777ec6 Version bump 5 months ago
J. King 44e2c9c13e Update documentation 5 months ago
J. King 866800dcc5 Finish last Guzzle-related tests 5 months ago
J. King 136d3782e3 Update changelog 6 months ago
J. King 3be3f43bab Start on tests for response wrappers 6 months ago
J. King d2f3f19128 Fix failures 6 months ago
J. King 459e44e041 Address remaining errors 6 months ago
J. King 56f015bfb9 More Guzzle conversion 6 months ago
J. King 64ec3f6ae4 Use unused variable 6 months ago
J. King 4d18bf27e2 Adjust most uses of Diactoros to Guzzle PSR-7 6 months ago
J. King e588a52e88 Replace ServerRequestFactory 6 months ago
J. King 6c0183faea Replace instances of Diactoros' EmptyResponse 6 months ago
J. King 560d4db139 Remove Diactoros in favour of Guzzle PSR-7 6 months ago
J. King 2557c22410 Update dependencies 8 months ago
J. King 4ca7b65a65 Update dependencies 8 months ago
J. King 4d37ae30ae Update dependencies 8 months ago
J. King d1da6fbe5e Use cases rather than casting bools to int in SQL 8 months ago
J. King d54733ad98 Update link to Nextcloud News documentation again 9 months ago
J. King a0c31fac5d Merge branch 'reader' 9 months ago
J. King 59358ec35b More PHP 7 fixes 9 months ago
J. King 90b66241b3 Fixes for PHP 7 9 months ago
J. King 761b3d5333 Return removed articles correctly in Miniflux 9 months ago
J. King d64dc751f9 Tests for query filters 9 months ago
J. King f51acb4264 Build exceptions correctly in Miniflux for clarity 9 months ago
J. King 300225439c Fix trivial error in Miniflux 9 months ago
J. King c6cc2a1a42 Restore coverage for Query class 9 months ago
J. King a44fe103d8 Prototype for nesting query filters 9 months ago
J. King 630536d789 Tests for union context 9 months ago
J. King 206c5c0012 Fill in union context 9 months ago
J. King 0c8f33c37c Remove setCTE and pushCTE from query builder 9 months ago
J. King 26e431b1a5 Simplify more queries 9 months ago
J. King 336207741d Add missing API documentation 9 months ago
J. King 6863c182d7 Update reference to the "Reeder" client 9 months ago
J. King f2aad7188c Update links to TT-RSS documentation 9 months ago
J. King 65b1bb4fcd Allow multiple dates in TT-RSS searches 9 months ago
J. King 2c5b9a6768 Fix missing TTRSS coverage 9 months ago
J. King 17832ac63e Allow timezone in TT-RSS search queries 9 months ago
J. King e65069885b Clean up obsolete FIXMEs 9 months ago
J. King 7e5d8494c4 Tests for selecting arrays of ranges 9 months ago
J. King e6505a5fda Work around possible MySQL bug 9 months ago
J. King 2acacd2647 Implement handling for arrays of ranges 9 months ago
J. King f6799e2ab1 Tests for date ranges in contexts 10 months ago
J. King 33a3478a58 Avoid use of PHP 7.4 feature 10 months ago
J. King 2489743d0f Further simplifications 10 months ago
J. King 0bd01849bb Remove unnecessary in() clause 10 months ago
J. King 895c045c9b Simplify folder selection in article queries 10 months ago
J. King fe02613214 Fix coverage 10 months ago
J. King 427bddd3b7 Allow multiple date ranges 10 months ago
J. King 53ba591720 Finish up article selection refactor 10 months ago
J. King 97dfef3267 Fix typos 10 months ago
J. King 396ca86482 Start on removal of conditional CTEs 10 months ago
J. King 4a87926dd5 Fix up context tests 10 months ago
J. King 6f1332c559 Start to shore up testing 10 months ago
J. King 308b592b18 Clean up coontext classes 10 months ago
J. King 983fa58ec8 Convert article and edition ranges to atomic 10 months ago
J. King 2c2bb4a856 Retrofits dates to use ranges 10 months ago
J. King c993168002 Update URL of Nextcloud News documentation 10 months ago
J. King 73497688fc Break contexts up into traits 10 months ago
J. King 1b0256d6ce Abandon automation of binary packaging for now 10 months ago
J. King 144a41e061 Prepare new version 10 months ago
J. King 60b4002329 Revert "Document that we actually emulate Miniflux 2.0.29" 10 months ago
J. King f24ec8b00b Address security vulnerability in Guzzle's PSR-7 10 months ago
J. King d379aa2253 Document that we actually emulate Miniflux 2.0.29 11 months ago
J. King b707ecc942 Tag new version 1 year ago
J. King afe26fb8e1 Style fixes 1 year ago
J. King 3a219a591d Update dependencies 1 year ago
J. King b5579d6e43 Support PHP 8.1 1 year ago
J. King b660508009 Improve MySQL test performance 1 year ago
J. King 3c884f521b Update dependencies 1 year ago
J. King 70b063e028 Make parts of generic packaging conditional 2 years ago
J. King cf3d270077 Merge branch 'deb' 2 years ago
J. King 1fa75aba4a Generate Debian source package without deb tooling 2 years ago
J. King 317d23c1bb Fix copy-paste error in manual 2 years ago
J. King 75dbe380ba Add Pandoc to AUR arsse-git build dependencies 2 years ago
  1. 1
      .gitignore
  2. 34
      CHANGELOG
  3. 341
      RoboFile.php
  4. 13
      UPGRADING
  5. 2
      arsse.php
  6. 23
      composer.json
  7. 684
      composer.lock
  8. 8
      dist/arch/PKGBUILD
  9. 10
      dist/arch/PKGBUILD-git
  10. 2
      dist/debian/control
  11. 2
      docs/en/020_Getting_Started/020_Download_and_Installation/999_ On_Other_Systems.md
  12. 2
      docs/en/020_Getting_Started/050_Configuration.md
  13. 1
      docs/en/030_Supported_Protocols/005_Miniflux.md
  14. 2
      docs/en/030_Supported_Protocols/010_Nextcloud_News.md
  15. 4
      docs/en/030_Supported_Protocols/020_Tiny_Tiny_RSS.md
  16. 4
      docs/en/040_Compatible_Clients.md
  17. 2
      lib/Arsse.php
  18. 4
      lib/Conf.php
  19. 27
      lib/Context/AbstractContext.php
  20. 35
      lib/Context/BooleanMembers.php
  21. 40
      lib/Context/Context.php
  22. 250
      lib/Context/ExclusionContext.php
  23. 262
      lib/Context/ExclusionMembers.php
  24. 20
      lib/Context/RootContext.php
  25. 50
      lib/Context/UnionContext.php
  26. 618
      lib/Database.php
  27. 6
      lib/Db/MySQL/Driver.php
  28. 3
      lib/Db/MySQL/PDODriver.php
  29. 2
      lib/Db/MySQL/Statement.php
  30. 2
      lib/Db/PDOStatement.php
  31. 2
      lib/Db/PostgreSQL/Driver.php
  32. 4
      lib/Db/PostgreSQL/Statement.php
  33. 5
      lib/Db/Result.php
  34. 3
      lib/Db/SQLite3/Driver.php
  35. 3
      lib/Db/SQLite3/PDODriver.php
  36. 2
      lib/Db/SQLite3/Statement.php
  37. 107
      lib/Feed.php
  38. 24
      lib/Feed/Item.php
  39. 27
      lib/Misc/HTTP.php
  40. 75
      lib/Misc/Query.php
  41. 75
      lib/Misc/QueryFilter.php
  42. 6
      lib/Misc/ValueInfo.php
  43. 10
      lib/REST.php
  44. 25
      lib/REST/Fever/API.php
  45. 19
      lib/REST/Miniflux/ErrorResponse.php
  46. 11
      lib/REST/Miniflux/Status.php
  47. 245
      lib/REST/Miniflux/V1.php
  48. 146
      lib/REST/NextcloudNews/V1_2.php
  49. 11
      lib/REST/NextcloudNews/Versions.php
  50. 44
      lib/REST/TinyTinyRSS/API.php
  51. 14
      lib/REST/TinyTinyRSS/Icon.php
  52. 42
      lib/REST/TinyTinyRSS/Search.php
  53. 2
      locale/en.php
  54. 2
      sql/SQLite3/0.sql
  55. 2
      sql/SQLite3/2.sql
  56. 5
      tests/bootstrap.php
  57. 2
      tests/cases/CLI/TestCLI.php
  58. 4
      tests/cases/Database/AbstractTest.php
  59. 66
      tests/cases/Database/SeriesArticle.php
  60. 2
      tests/cases/Database/SeriesFeed.php
  61. 2
      tests/cases/Database/SeriesLabel.php
  62. 3
      tests/cases/Database/SeriesTag.php
  63. 4
      tests/cases/Db/BaseDriver.php
  64. 2
      tests/cases/Db/SQLite3/TestCreation.php
  65. 2
      tests/cases/Db/SQLite3PDO/TestCreation.php
  66. 48
      tests/cases/Feed/TestFeed.php
  67. 2
      tests/cases/ImportExport/TestImportExport.php
  68. 182
      tests/cases/Misc/TestContext.php
  69. 27
      tests/cases/Misc/TestHTTP.php
  70. 61
      tests/cases/Misc/TestQuery.php
  71. 171
      tests/cases/Misc/TestValueInfo.php
  72. 62
      tests/cases/REST/Fever/TestAPI.php
  73. 22
      tests/cases/REST/Miniflux/TestErrorResponse.php
  74. 17
      tests/cases/REST/Miniflux/TestStatus.php
  75. 465
      tests/cases/REST/Miniflux/TestV1.php
  76. 211
      tests/cases/REST/NextcloudNews/TestV1_2.php
  77. 11
      tests/cases/REST/NextcloudNews/TestVersions.php
  78. 69
      tests/cases/REST/TestREST.php
  79. 261
      tests/cases/REST/TinyTinyRSS/TestAPI.php
  80. 24
      tests/cases/REST/TinyTinyRSS/TestIcon.php
  81. 22
      tests/cases/REST/TinyTinyRSS/TestSearch.php
  82. 1
      tests/cases/Service/TestDaemon.php
  83. 2
      tests/cases/User/TestUser.php
  84. 31
      tests/lib/AbstractTest.php
  85. 55
      tests/lib/DatabaseDrivers/MySQL.php
  86. 60
      tests/lib/DatabaseDrivers/MySQLCommon.php
  87. 15
      tests/lib/DatabaseDrivers/MySQLPDO.php
  88. 60
      tests/lib/DatabaseDrivers/PostgreSQL.php
  89. 68
      tests/lib/DatabaseDrivers/PostgreSQLCommon.php
  90. 14
      tests/lib/DatabaseDrivers/PostgreSQLPDO.php
  91. 62
      tests/lib/DatabaseDrivers/SQLite3.php
  92. 70
      tests/lib/DatabaseDrivers/SQLite3Common.php
  93. 19
      tests/lib/DatabaseDrivers/SQLite3PDO.php
  94. 1
      tests/phpunit.dist.xml
  95. 668
      vendor-bin/csfixer/composer.lock
  96. 675
      vendor-bin/daux/composer.lock
  97. 5
      vendor-bin/phpstan/composer.json
  98. 79
      vendor-bin/phpstan/composer.lock
  99. 696
      vendor-bin/phpunit/composer.lock
  100. 882
      vendor-bin/robo/composer.lock

1
.gitignore

@ -11,6 +11,7 @@
/arsse.db*
/config.php
/.php_cs.cache
/.php-cs-fixer.cache
/tests/.phpunit.result.cache
# Dependencies

34
CHANGELOG

@ -1,3 +1,37 @@
Version 0.??.? (????-??-??)
===========================
Changes:
- Require PHP 7.3
Version 0.10.4 (2023-01-24)
===========================
Changes:
- Support PHP 8.2
Version 0.10.3 (2022-09-14)
===========================
Bug fixes:
- Return all removed articles when multiple statuses are requested in Miniflux
- Allow multiple date ranges in search strings in Tiny Tiny RSS
- Honour user time zone when interpreting search strings in Tiny Tiny RSS
- Perform MySQL table maintenance more reliably
- Address CVE-2022-31090, CVE-2022-31091, CVE-2022-29248, and CVE-2022-31109
Version 0.10.2 (2022-04-04)
===========================
Changes:
- Update Guzzle PSR-7 due to CVE-2022-24775
Version 0.10.1 (2022-01-17)
===========================
Changes:
- Support PHP 8.1
Version 0.10.0 (2021-07-11)
===========================

341
RoboFile.php

@ -1,6 +1,5 @@
<?php
use Robo\Collection\CollectionBuilder;
use Robo\Result;
const BASE = __DIR__.\DIRECTORY_SEPARATOR;
@ -166,7 +165,7 @@ class RoboFile extends \Robo\Tasks {
(IS_WIN && (!exec(escapeshellarg($bin)." --help $blackhole", $junk, $status) || $status))
|| (!IS_WIN && (!exec("which ".escapeshellarg($bin)." $blackhole", $junk, $status) || $status))
) {
return false;
return false;
}
}
return true;
@ -188,7 +187,10 @@ class RoboFile extends \Robo\Tasks {
}
// establish which commit to package
[$commit, $version] = $this->commitVersion($commit);
$archVersion = preg_replace('/^([^-]+)-(\d+)-(\w+)$/', "$1.r$2.$3", $version);
preg_match('/^([^-]+)(?:-(\d+)-(\w+))?$/', $version, $m);
$archVersion = $m[1].($m[2] ? ".r$m[2].$m[3]" : "");
$baseVersion = $m[1];
$release = $m[2];
// name the generic release tarball
$tarball = BASE."release/$version/arsse-$version.tar.gz";
// start a collection
@ -201,24 +203,47 @@ class RoboFile extends \Robo\Tasks {
return $result;
}
try {
// generate the Debian changelog; this also validates our original changelog
$debianChangelog = $this->changelogDebian($this->changelogParse(file_get_contents($dir."CHANGELOG"), $version), $version);
// Perform Arch-specific tasks
if (file_exists($dir."dist/arch")) {
// patch the Arch PKGBUILD file with the correct version string
$t->addTask($this->taskReplaceInFile($dir."dist/arch/PKGBUILD")->regex('/^pkgver=.*$/m')->to("pkgver=$archVersion"));
// patch the Arch PKGBUILD file with the correct source file
$t->addTask($this->taskReplaceInFile($dir."dist/arch/PKGBUILD")->regex('/^source=\("arsse-[^"]+"\)$/m')->to('source=("'.basename($tarball).'")'));
}
// perform Debian-specific tasks
if (file_exists($dir."dist/debian")) {
// generate the Debian changelog; this also validates our original changelog
$changelog = $this->changelogParse(file_get_contents($dir."CHANGELOG"), $version);
$debianChangelog = $this->changelogDebian($changelog, $version);
// save the Debian-format changelog
$t->addTask($this->taskWriteToFile($dir."dist/debian/changelog")->text($debianChangelog));
}
// perform RPM-specific tasks
if (file_exists($dir."dist/rpm")) {
// patch the spec file with the correct version and release
$t->addTask($this->taskReplaceInFile($dir."dist/rpm/arsse.spec")->regex('/^Version: .*$/m')->to("Version: $baseVersion"));
$t->addTask($this->taskReplaceInFile($dir."dist/rpm/arsse.spec")->regex('/^Release: .*$/m')->to("Release: $release"));
// patch the spec file with the correct tarball name
$t->addTask($this->taskReplaceInFile($dir."dist/rpm/arsse.spec")->regex('/^Source0: .*$/m')->to("Source0: arsse-$version.tar.gz"));
// append the RPM changelog to the spec file
$t->addTask($this->taskWriteToFile($dir."dist/rpm/arsse.spec")->append(true)->text("\n\n%changelog\n".$this->changelogRPM($changelog, $version)));
}
// save commit description to VERSION file for reference
$t->addTask($this->taskWriteToFile($dir."VERSION")->text($version));
// patch the Arch PKGBUILD file with the correct version string
$t->addTask($this->taskReplaceInFile($dir."dist/arch/PKGBUILD")->regex('/^pkgver=.*$/m')->to("pkgver=$archVersion"));
// patch the Arch PKGBUILD file with the correct source file
$t->addTask($this->taskReplaceInFile($dir."dist/arch/PKGBUILD")->regex('/^source=\("arsse-[^"]+"\)$/m')->to('source=("'.basename($tarball).'")'));
// save the Debian-format changelog
$t->addTask($this->taskWriteToFile($dir."dist/debian/changelog")->text($debianChangelog));
// perform Composer installation in the temp location with dev dependencies
$t->addTask($this->taskComposerInstall()->arg("-q")->dir($dir));
// generate manpages
$t->addTask($this->taskExec("./robo manpage")->dir($dir));
// generate the HTML manual
$t->addTask($this->taskExec("./robo manual -q")->dir($dir));
if (file_exists($dir."docs") || file_exists($dir."manpages")) {
// perform Composer installation in the temp location with dev dependencies to include Robo and Daux
$t->addTask($this->taskExec("composer install")->arg("-q")->dir($dir));
}
if (file_exists($dir."manpages")) {
// generate manpages
$t->addTask($this->taskExec("./robo manpage")->dir($dir));
}
if (file_exists($dir."docs")) {
// generate the HTML manual
$t->addTask($this->taskExec("./robo manual -q")->dir($dir));
}
// perform Composer installation in the temp location for final output
$t->addTask($this->taskComposerInstall()->dir($dir)->noDev()->optimizeAutoloader()->arg("--no-scripts")->arg("-q"));
$t->addTask($this->taskExec("composer install")->dir($dir)->arg("--no-dev")->arg("-o")->arg("--no-scripts")->arg("-q"));
// delete unwanted files
$t->addTask($this->taskFilesystemStack()->remove([
$dir.".git",
@ -270,117 +295,109 @@ class RoboFile extends \Robo\Tasks {
return $result;
}
/** Packages a given commit of the software into an Arch package
/** Packages a release tarball into a Debian source package
*
* The commit to package may be any Git tree-ish identifier: a tag, a branch,
* or any commit hash. If none is provided on the command line, Robo will prompt
* for a commit to package; the default is "HEAD".
*
* The Arch base-devel group should be installed for this.
*/
public function packageArch(string $commit = null): Result {
if (!$this->toolExists("git", "makepkg", "updpkgsums")) {
throw new \Exception("Git, makepkg, and updpkgsums are required in PATH to produce Arch packages");
}
public function packageDebsrc(string $commit = null): Result {
// establish which commit to package
[$commit, $version] = $this->commitVersion($commit);
$tarball = BASE."release/$version/arsse-$version.tar.gz";
$dir = dirname($tarball).\DIRECTORY_SEPARATOR;
// start a collection
// determine the base version (i.e. x.y.z) and the Debian version (i.e. x.y.z-a)
preg_match('/^(\d+(?:\.\d+)+)(?:-(\d+))?/', $version, $m);
$baseVersion = $m[1];
$debVersion = $m[1]."-".($version === $baseVersion ? "1" : $m[2]);
// start a task collection and create a temporary directory
$t = $this->collectionBuilder();
$dir = $t->tmpDir().\DIRECTORY_SEPARATOR;
// build the generic release tarball if it doesn't exist
if (!file_exists($tarball)) {
$t->addTask($this->taskExec(BASE."robo package:generic $commit"));
}
// extract the PKGBUILD from the tarball
$t->addCode(function() use ($tarball, $dir) {
// because Robo doesn't support extracting a single file we have to do it ourselves
(new \Archive_Tar($tarball))->extractList("arsse/dist/arch/PKGBUILD", $dir, "arsse/dist/arch/", false);
// perform a do-nothing filesystem operation since we need a Robo task result
return $this->taskFilesystemStack()->chmod($dir."PKGBUILD", 0644)->run();
})->completion($this->taskFilesystemStack()->remove($dir."PKGBUILD"));
// build the package
$t->addTask($this->taskExec("makepkg -Ccf")->dir($dir));
$base = $dir."arsse-$version".\DIRECTORY_SEPARATOR;
// start by extracting the tarball
$t->addCode(function() use ($tarball, $dir, $base) {
// Robo's extract task is broken, so we do it manually
(new \Archive_Tar($tarball))->extract($dir, false);
return $this->taskFilesystemStack()->rename($dir."arsse", $base)->run();
});
// re-pack the tarball using a specific name special to Debian
$t->addTask($this->taskPack($dir."arsse_$baseVersion.orig.tar.gz")->addDir("arsse-$baseVersion", $base));
// pack the debian tarball
$t->addTask($this->taskPack($dir."arsse_$debVersion.debian.tar.gz")->addDir("debian", $base."dist/debian"));
// generate the DSC file
$t->addCode(function() use ($t, $debVersion, $baseVersion, $dir, $base) {
try {
$dsc = $this->generateDebianSourceControl($base."dist/debian/", $debVersion, [$dir."arsse_$baseVersion.orig.tar.gz", $dir."arsse_$debVersion.debian.tar.gz"]);
} catch (\Exception $e) {
return new Result($t, 1, $e->getMessage());
}
// write the DSC file
return $this->taskWriteToFile($dir."arsse_$debVersion.dsc")->text($dsc)->run();
});
// delete any existing files
$t->AddTask($this->taskFilesystemStack()->remove([BASE."release/$version/arsse_$baseVersion.orig.tar.gz", BASE."release/$version/arsse_$debVersion.debian.tar.gz", BASE."release/$version/arsse_$debVersion.dsc"]));
// copy the new files over
$t->addTask($this->taskFilesystemStack()->copy($dir."arsse_$baseVersion.orig.tar.gz", BASE."release/$version/arsse_$baseVersion.orig.tar.gz")->copy($dir."arsse_$debVersion.debian.tar.gz", BASE."release/$version/arsse_$debVersion.debian.tar.gz")->copy($dir."arsse_$debVersion.dsc", BASE."release/$version/arsse_$debVersion.dsc"));
return $t->run();
}
/** Packages a given commit of the software into source and binary Debian packages
/** Packages a given commit of the software and produces all relevant release files
*
* The commit to package may be any Git tree-ish identifier: a tag, a branch,
* or any commit hash. If none is provided on the command line, Robo will prompt
* for a commit to package; the default is "HEAD".
*
* The pbuilder tool should be installed for this.
*/
public function packageDebian(string $commit = null): Result {
if (!$this->toolExists("git", "sudo", "pbuilder")) {
throw new \Exception("Git, sudo, and pbuilder are required in PATH to produce Debian packages");
}
// establish which commit to package
[$commit, $version] = $this->commitVersion($commit);
$tarball = BASE."release/$version/arsse-$version.tar.gz";
// define some more variables
$tgz = BASE."release/pbuilder-arsse.tgz";
$bind = dirname($tarball);
$script = BASE."dist/debian/pbuilder.sh";
$user = trim(`id -un`);
$group = trim(`id -gn`);
// start a task collection
$t = $this->collectionBuilder();
// check that the pbuilder base exists and create it if it does not
if (!file_exists($tgz)) {
$t->addTask($this->taskFilesystemStack()->mkdir(BASE."release"));
$t->addTask($this->taskExec('sudo pbuilder create --basetgz '.escapeshellarg($tgz).' --mirror http://ftp.ca.debian.org/debian/ --extrapackages "debhelper devscripts lintian"'));
}
// build the generic release tarball if it doesn't exist
if (!file_exists($tarball)) {
$t->addTask($this->taskExec(BASE."robo package:generic $commit"));
}
// build the packages
$t->addTask($this->taskExec('sudo pbuilder execute --basetgz '.escapeshellarg($tgz).' --bindmounts '.escapeshellarg($bind).' -- '.escapeshellarg($script).' '.escapeshellarg("$bind/".basename($tarball))));
// take ownership of the output files
$t->addTask($this->taskExec("sudo chown -R $user:$group ".escapeshellarg($bind)));
return $t->run();
}
/** Generates all possible package types for a given commit of the software
*
* The commit to package may be any Git tree-ish identifier: a tag, a branch,
* or any commit hash. If none is provided on the command line, Robo will prompt
* for a commit to package; the default is "HEAD".
*
* Generic release tarballs will always be generated, but distribution-specific
* packages are skipped when the required tools are not available
* In addition to the release tarball, a Debian source package, Arch PKGBUILD,
* and RPM spec file are output as well. These are suitable for use with Open
* Build Service instances and with slight modification the Arch User Repository.
* Use for Launchpad PPAs has not been tested.
*/
public function package(string $commit = null): Result {
if (!$this->toolExists("git")) {
throw new \Exception("Git is required in PATH to produce packages");
}
[$commit,] = $this->commitVersion($commit);
// determine whether the distribution-specific packages can be built
$dist = [
'Arch' => $this->toolExists("git", "makepkg", "updpkgsums"),
'Debian' => $this->toolExists("git", "sudo", "pbuilder"),
];
[$commit, $version] = $this->commitVersion($commit);
$tarball = BASE."release/$version/arsse-$version.tar.gz";
// build the generic release tarball
$result = $this->taskExec(BASE."robo package:generic $commit")->run();
if (!$result->wasSuccessful()) {
return $result;
}
// if the generic tarball could be built, try to produce Arch, Debian, and RPM files; these might legitimately not exist in old releases
// start by getting the list of files from the tarball
$archive = new \Archive_Tar($tarball);
$filelist = array_flip(array_column($archive->listContent(), "filename"));
// start a collection
$t = $this->collectionBuilder();
// build the generic release tarball
$t->addTask($this->taskExec(BASE."robo package:generic $commit"));
// build other packages
foreach ($dist as $distro => $run) {
if ($run) {
$subcmd = strtolower($distro);
$t->addTask($this->taskExec(BASE."robo package:$subcmd $commit"));
}
// Produce an Arch PKGBUILD if appropriate
if (isset($filelist['arsse/dist/arch/PKGBUILD'])) {
$t->addCode(function() use ($tarball, $archive) {
$dir = dirname($tarball).\DIRECTORY_SEPARATOR;
$archive->extractList("arsse/dist/arch/PKGBUILD", $dir, "arsse/dist/arch/", false);
// update the tarball's checksum
$sums = [
'md5' => hash_file("md5", $tarball),
];
return $this->taskReplaceInFile($dir."PKGBUILD")->regex('/^md5sums=\("SKIP"\)$/m')->to('md5sums=("'.$sums['md5'].'")')->run();
});
}
$out = $t->run();
// note any packages which were not built
foreach ($dist as $distro => $run) {
if (!$run) {
$this->say("Packages for $distro skipped");
}
// Produce a Debian source package if appropriate
if (isset($filelist['arsse/dist/debian/control']) && isset($filelist['arsse/dist/debian/source/format'])) {
$t->addTask($this->taskExec(BASE."robo package:debsrc $commit"));
}
return $out;
// Produce an RPM spec file if appropriate
if (isset($filelist['arsse/dist/rpm/arsse.spec'])) {
$t->addCode(function() use ($tarball, $archive) {
$dir = dirname($tarball).\DIRECTORY_SEPARATOR;
$archive->extractList("arsse/dist/rpm/arsse.spec", $dir, "arsse/dist/rpm/", false);
// perform a do-nothing filesystem operation since we need a Robo task result
return $this->taskFilesystemStack()->chmod($dir."arsse.spec", 0644)->run();
});
}
return $t->run();
}
/** Generates static manual pages in the "manual" directory
@ -569,4 +586,130 @@ class RoboFile extends \Robo\Tasks {
}
return $out;
}
protected function generateDebianSourceControl(string $dir, string $version, array $tarballs): string {
// read in control file
if (!$control = @file_get_contents($dir."control")) {
throw new \Exception("Unable to read Debian control file");
}
// read the format
if (!$format = @file_get_contents($dir."source/format")) {
throw new \Exception("Unable to read source format in Debian files");
}
// read the binary packages from the control file
if (preg_match_all('/^Package:\s*(\S+)/m', $control, $m)) {
$binary = [];
foreach ($m[1] as $pkg) {
$binary[] = $pkg;
}
} else {
throw new \Exception("No packages defined in Debian control file");
}
// read the package architectures from the control file
if (preg_match_all('/^Architecture:\s*(\S+)/m', $control, $m) || sizeof($m[1]) != sizeof($binary)) {
$architecture = [];
foreach ($m[1] as $pkg) {
$architecture[] = preg_replace('/\s/', "", $pkg);
}
} else {
throw new \Exception("Number of architectures defined in Debian control file does not match number of packages");
}
// read the package sections from the control file
if (preg_match_all('/^Section:\s*(\S+)/m', $control, $m) || sizeof($m[1]) != sizeof($binary)) {
$section = [];
foreach ($m[1] as $pkg) {
$section[] = $pkg;
}
} else {
throw new \Exception("Number of sections defined in Debian control file does not match number of packages");
}
// read the package priorities from the control file
if (preg_match_all('/^Priority:\s*(\S+)/m', $control, $m) || sizeof($m[1]) != sizeof($binary)) {
$priority = [];
foreach ($m[1] as $pkg) {
$priority[] = $pkg;
}
} else {
throw new \Exception("Number of priorities defined in Debian control file does not match number of packages");
}
// read simple metadata from the control file
$metadata = [];
foreach (["Source", "Maintainer", "Homepage", "Standards-Version", "Vcs-Browser", "Vcs-Git"] as $meta) {
if (preg_match('/^'.$meta.':\s*(.+)/m', $control, $m)) {
$metadata[$meta] = $m[1];
} else {
throw new \Exception("$meta is not defined in Debian control file");
}
}
// read build dependencies from control file
if (preg_match('/(?:^|\n)Build-Depends:\s*((?:[^\n]|\n(?= ))+)/s', $control, $m)) {
$buildDepends = preg_replace('/\s/', "", $m[1]);
} else {
$buildDepends = "";
}
// trim format
$format = trim($format);
// consolidate binaries and package list
$packageList = [];
for ($a = 0; $a < sizeof($binary); $a++) {
$packageList[] = "$binary[$a] deb $section[$a] $priority[$a] arch=$architecture[$a]";
}
$packageList = implode("\n ", $packageList);
// consolidate package names
$binary = implode(",", $binary);
// consolidate architectures
$architecture = implode(",", array_unique($architecture));
// calculate checksums for files
$fMeta = [];
foreach ($tarballs as $f) {
$fMeta[$f] = [
'name' => basename($f),
'size' => filesize($f),
'sha1' => hash_file("sha1", $f),
'sha256' => hash_file("sha256", $f),
'md5' => hash_file("md5", $f),
];
}
// consolidate SHA-1 checksums
$sums = [];
foreach ($fMeta as $data) {
$sums[] = $data['sha1']." ".$data['size']." ".$data['name'];
}
$sumsSha1 = implode("\n ", $sums);
// consolidate SHA-256 checksums
$sums = [];
foreach ($fMeta as $data) {
$sums[] = $data['sha256']." ".$data['size']." ".$data['name'];
}
$sumsSha256 = implode("\n ", $sums);
// consolidate MD5 checksums
$sums = [];
foreach ($fMeta as $data) {
$sums[] = $data['md5']." ".$data['size']." ".$data['name'];
}
$sumsMd5 = implode("\n ", $sums);
// return complete file
return <<< DSC_FILE
Format: $format
Source: {$metadata['Source']}
Binary: $binary
Architecture: $architecture
Version: $version
Maintainer: {$metadata['Maintainer']}
Homepage: {$metadata['Homepage']}
Standards-Version: {$metadata['Standards-Version']}
Vcs-Browser: {$metadata['Vcs-Browser']}
Vcs-Git: {$metadata['Vcs-Git']}
Build-Depends: $buildDepends
Package-List:
$packageList
Checksums-Sha1:
$sumsSha1
Checksums-Sha256:
$sumsSha256
Files:
$sumsMd5
DSC_FILE;
}
}

13
UPGRADING

@ -9,6 +9,19 @@ usually prudent:
- Check for any changes to sample systemd unit or other init files
- If installing from source, update dependencies with:
`composer install -o --no-dev`
Upgrading from 0.10.2 to 0.10.3
=============================
- PHP 7.3 is now required
Upgrading from 0.10.2 to 0.10.3
=============================
- The following Composer dependencies have been removed:
- laminas/laminas-diactoros
Upgrading from 0.8.5 to 0.9.0

2
arsse.php

@ -13,7 +13,7 @@ require_once BASE."vendor".DIRECTORY_SEPARATOR."autoload.php";
ignore_user_abort(true);
ini_set("memory_limit", "-1");
ini_set("max_execution_time", "0");
// FIXME: This is required by a dependency of Picofeed
// NOTE: While running in the wild we don't want to spew deprecation warnings if users are ahead of us in PHP versions
error_reporting(\E_ALL & ~\E_DEPRECATED);
if (\PHP_SAPI === "cli") {

23
composer.json

@ -24,11 +24,11 @@
"ext-hash": "*",
"ext-filter": "*",
"ext-dom": "*",
"nicolus/picofeed": "^0.1.43",
"nicolus/picofeed": "dev-bugfix",
"hosteurope/password-generator": "1.*",
"docopt/docopt": "1.*",
"docopt/docopt": "dev-master",
"jkingweb/druuid": "3.*",
"laminas/laminas-diactoros": "2.*",
"guzzlehttp/psr7": "1.*",
"laminas/laminas-httphandlerrunner": "1.*"
},
"require-dev": {
@ -39,7 +39,10 @@
},
"config": {
"platform": {
"php": "7.1.33"
"php": "7.3.33"
},
"allow-plugins": {
"bamarni/composer-bin-plugin": true
}
},
"scripts": {
@ -56,5 +59,15 @@
"JKingWeb\\Arsse\\Test\\": "tests/lib/",
"JKingWeb\\Arsse\\TestCase\\": "tests/cases/"
}
}
},
"repositories": [
{
"type": "vcs",
"url": "https://github.com/rhukster/picoFeed"
},
{
"type": "vcs",
"url": "https://github.com/docopt/docopt.php"
}
]
}

684
composer.lock

File diff suppressed because it is too large

8
dist/arch/PKGBUILD

@ -1,6 +1,6 @@
# Maintainer: J. King <jking@jkingweb.ca>
pkgname="arsse"
pkgver=0.9.2
pkgver=0.10.4
pkgrel=1
epoch=
pkgdesc="Multi-protocol RSS/Atom newsfeed synchronization server"
@ -14,7 +14,7 @@ optdepends=("nginx: HTTP server"
"apache>=2.4: HTTP server"
"percona-server: Alternate database"
"postgresql>=10: Alternate database"
"php-pgsql>=7.1: PostgreSQL database support")
"php-pgsql>=7.3: PostgreSQL database support")
backup=("etc/webapps/arsse/config.php"
"etc/php/php-fpm.d/arsse.conf"
"etc/webapps/arsse/nginx/example.conf"
@ -24,12 +24,12 @@ backup=("etc/webapps/arsse/config.php"
"etc/webapps/arsse/apache/example.conf"
"etc/webapps/arsse/apache/arsse.conf"
"etc/webapps/arsse/apache/arsse-loc.conf")
source=("arsse-0.9.2.tar.gz")
source=("arsse-0.10.4.tar.gz")
md5sums=("SKIP")
package() {
# define runtime dependencies
depends=("php>=7.1" "php-intl>=7.1" "php-sqlite>=7.1" "php-fpm>=7.1")
depends=("php>=7.3" "php-intl>=7.3" "php-sqlite>=7.3" "php-fpm>=7.3")
# create most directories necessary for the final package
cd "$pkgdir"
mkdir -p "usr/share/webapps/arsse" "usr/share/doc/arsse" "usr/share/licenses/arsse" "usr/lib/systemd/system" "usr/lib/sysusers.d" "usr/lib/tmpfiles.d" "etc/php/php-fpm.d" "etc/webapps/arsse"

10
dist/arch/PKGBUILD-git

@ -1,6 +1,6 @@
# Maintainer: J. King <jking@jkingweb.ca>
pkgname="arsse-git"
pkgver=0.9.2
pkgver=0.10.4
pkgrel=1
epoch=
pkgdesc="Multi-protocol RSS/Atom newsfeed synchronization server, bugfix-testing version"
@ -9,14 +9,14 @@ url="https://thearsse.com/"
license=("MIT")
provides=("arsse")
conflicts=("arsse")
depends=("php>=7.1" "php-intl>=7.1" "php-sqlite>=7.1")
makedepends=("composer")
depends=("php>=7.3" "php-intl>=7.3" "php-sqlite>=7.3")
makedepends=("composer" "pandoc")
checkdepends=()
optdepends=("nginx: HTTP server"
"apache>=2.4: HTTP server"
"percona-server: Alternate database"
"postgresql>=10: Alternate database"
"php-pgsql>=7.1: PostgreSQL database support")
"php-pgsql>=7.3: PostgreSQL database support")
backup=("etc/webapps/arsse/config.php"
"etc/php/php-fpm.d/arsse.conf"
"etc/webapps/arsse/nginx/example.conf"
@ -46,7 +46,7 @@ build() {
package() {
# define runtime dependencies
depends=("php>=7.1" "php-intl>=7.1" "php-sqlite>=7.1" "php-fpm>=7.1")
depends=("php>=7.3" "php-intl>=7.3" "php-sqlite>=7.3" "php-fpm>=7.3")
# create most directories necessary for the final package
cd "$pkgdir"
mkdir -p "usr/share/webapps/arsse" "usr/share/doc/arsse" "usr/share/licenses/arsse" "usr/lib/systemd/system" "usr/lib/sysusers.d" "usr/lib/tmpfiles.d" "etc/php/php-fpm.d" "etc/webapps/arsse"

2
dist/debian/control

@ -20,7 +20,7 @@ Description: Multi-protocol RSS/Atom newsfeed synchronization server
server.
Depends: ${misc:Depends},
dbconfig-sqlite3 | dbconfig-pgsql | dbconfig-mysql | dbconfig-no-thanks,
php (>= 7.1.0),
php (>= 7.3.0),
php-cli,
php-intl,
php-json,

2
docs/en/020_Getting_Started/020_Download_and_Installation/999_ On_Other_Systems.md

@ -11,7 +11,7 @@ The Arsse has the following requirements:
- A Web server such as:
- [Nginx](https://nginx.org)
- [Apache HTTP server](https://httpd.apache.org) 2.4 or later
- PHP 7.1.0 or later with the following extensions:
- PHP 7.3.0 or later with the following extensions:
- [intl](https://php.net/manual/en/book.intl.php), [json](https://php.net/manual/en/book.json.php), [hash](https://php.net/manual/en/book.hash.php), [filter](https://php.net/manual/en/book.filter.php), and [dom](https://php.net/manual/en/book.dom.php)
- [simplexml](https://php.net/manual/en/book.simplexml.php), and [iconv](https://php.net/manual/en/book.iconv.php)
- One of:

2
docs/en/020_Getting_Started/050_Configuration.md

@ -2,7 +2,7 @@
Depending on how The Arsse was installed, it will look for configuration in the following places:
| Installation method | Default database path |
| Installation method | Default configuration file path |
|---------------------|--------------------------------------------------------|
| Arch Linux package | `/etc/webapps/arsse/config.php` |
| Debian package | `/etc/arsse/config.php` |

1
docs/en/030_Supported_Protocols/005_Miniflux.md

@ -39,7 +39,6 @@ Miniflux version 2.0.28 is emulated, though not all features are implemented
- Filtering rules may not function identically (see below for details)
- The `checked_at` field of feeds indicates when the feed was last updated rather than when it was last checked
- Creating a feed with the `scrape` property set to `true` might not return scraped content for the initial synchronization
- Querying articles for both read/unread and removed statuses will not return all removed articles
- Search strings will match partial words
- OPML import either succeeds or fails atomically: if one feed fails, no feeds are imported

2
docs/en/030_Supported_Protocols/010_Nextcloud_News.md

@ -10,7 +10,7 @@
<dt>API endpoint</dt>
<dd>/index.php/apps/news/api/v1-2/</dd>
<dt>Specifications</dt>
<dd><a href="https://github.com/nextcloud/news/blob/master/docs/externalapi/Legacy.md">Version 1.2</a></dd>
<dd><a href="https://github.com/nextcloud/news/blob/master/docs/api/api-v1-2.md">Version 1.2</a></dd>
</dl>
The Nextcloud News protocol was the first supported by The Arsse, and has been supported in full since version 0.3.0.

4
docs/en/030_Supported_Protocols/020_Tiny_Tiny_RSS.md

@ -10,7 +10,7 @@
<dt>API endpoint</dt>
<dd>/tt-rss/api</dd>
<dt>Specifications</dt>
<dd><a href="https://git.tt-rss.org/git/tt-rss/wiki/ApiReference">Main</a>, <a href="https://git.tt-rss.org/fox/tt-rss/wiki/SearchSyntax">search syntax</a>, <a href="https://github.com/jangernert/FeedReader/blob/master/data/tt-rss-feedreader-plugin/README.md">FeedReader extensions</a>, <a href="https://github.com/hrk/tt-rss-newsplus-plugin/blob/master/README.md">News+ extension</a></dd>
<dd><a href="https://tt-rss.org/wiki/ApiReference">Main</a>, <a href="https://tt-rss.org/wiki/SearchSyntax">search syntax</a>, <a href="https://github.com/jangernert/FeedReader/blob/master/data/tt-rss-feedreader-plugin/README.md">FeedReader extensions</a>, <a href="https://github.com/hrk/tt-rss-newsplus-plugin/blob/master/README.md">News+ extension</a></dd>
</dl>
The Arsse supports not only the Tiny Tiny RSS protocol, but also extensions required by the FeedReader client and the more commonly supported `getCompactHeadlines` extension.
@ -37,7 +37,7 @@ The Arsse does not currently support the entire protocol. Notably missing featur
- 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"`
- 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)
- Specifying multiple non-negative dates usually returns no results as articles must match all specified dates simultaneously; The Arsse instead returns articles matching any of the specified dates
- Dates are always relative to UTC
- Full-text search is not yet employed with any database, including PostgreSQL
- Article hashes are normally SHA1; The Arsse uses SHA256 hashes

4
docs/en/040_Compatible_Clients.md

@ -121,14 +121,14 @@ The Arsse does not at this time have any first party clients. However, because T
</td>
</tr>
<tr>
<td><a href="https://reeder.app/">Reeder</a></td>
<td><a href="https://reeder.app/">Reeder 3</a></td>
<td>macOS</td>
<td class="N"></td>
<td class="N"></td>
<td class="N"></td>
<td class="Y"></td>
<td>
<p>Also available for iOS.</p>
<p>Also available for iOS. Reeder 5 no longer supports the Fever protocol.</p>
</td>
</tr>
<tr>

2
lib/Arsse.php

@ -7,7 +7,7 @@ declare(strict_types=1);
namespace JKingWeb\Arsse;
class Arsse {
public const VERSION = "0.10.0";
public const VERSION = "0.10.4";
public const REQUIRED_EXTENSIONS = [
"intl", // as this extension is required to prepare formatted messages, its absence will throw a distinct English-only exception
"dom",

4
lib/Conf.php

@ -7,13 +7,15 @@
declare(strict_types=1);
namespace JKingWeb\Arsse;
use AllowDynamicProperties;
use JKingWeb\Arsse\Misc\ValueInfo as Value;
/** Class for loading, saving, and querying configuration
*
* The Conf class serves both as a means of importing and querying configuration information, as well as a source for default parameters when a configuration file does not specify a value.
* All public properties are configuration parameters that may be set by the server administrator. */
class Conf {
#[AllowDynamicProperties]
class Conf {
/** @var string Default language to use for logging and errors */
public $lang = "en";

27
lib/Context/AbstractContext.php

@ -0,0 +1,27 @@
<?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;
abstract class AbstractContext {
protected $props = [];
protected $parent = null;
protected function act(string $prop, int $set, $value) {
if ($set) {
if (is_null($value)) {
unset($this->props[$prop]);
$this->$prop = (new \ReflectionClass($this))->getDefaultProperties()[$prop];
} else {
$this->props[$prop] = true;
$this->$prop = $value;
}
return $this->parent ?? $this;
} else {
return isset($this->props[$prop]);
}
}
}

35
lib/Context/BooleanMembers.php

@ -0,0 +1,35 @@
<?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;
trait BooleanMembers {
public $unread = null;
public $starred = null;
public $hidden = null;
public $labelled = null;
public $annotated = null;
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 hidden(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);
}
}

40
lib/Context/Context.php

@ -6,16 +6,12 @@
declare(strict_types=1);
namespace JKingWeb\Arsse\Context;
class Context extends ExclusionContext {
class Context extends RootContext {
use BooleanMembers;
use ExclusionMembers;
/** @var ExclusionContext */
public $not;
public $limit = 0;
public $offset = 0;
public $unread;
public $starred;
public $hidden;
public $labelled;
public $annotated;
public function __construct() {
$this->not = new ExclusionContext($this);
@ -30,32 +26,4 @@ class Context extends ExclusionContext {
public function __destruct() {
unset($this->not);
}
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 hidden(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);
}
}

250
lib/Context/ExclusionContext.php

@ -6,53 +6,18 @@
declare(strict_types=1);
namespace JKingWeb\Arsse\Context;
use JKingWeb\Arsse\Misc\ValueInfo;
use JKingWeb\Arsse\Misc\Date;
class ExclusionContext extends AbstractContext {
use ExclusionMembers;
class ExclusionContext {
public $folder;
public $folders;
public $folderShallow;
public $foldersShallow;
public $tag;
public $tags;
public $tagName;
public $tagNames;
public $subscription;
public $subscriptions;
public $edition;
public $editions;
public $article;
public $articles;
public $label;
public $labels;
public $labelName;
public $labelNames;
public $annotationTerms;
public $searchTerms;
public $titleTerms;
public $authorTerms;
public $oldestArticle;
public $latestArticle;
public $oldestEdition;
public $latestEdition;
public $modifiedSince;
public $notModifiedSince;
public $markedSince;
public $notMarkedSince;
protected $props = [];
protected $parent;
public function __construct(self $c = null) {
$this->parent = $c;
public function __construct(Context $parent = null) {
$this->parent = $parent;
}
public function __clone() {
// if the context was cloned because its parent was cloned, change the parent to the clone
if ($this->parent) {
$t = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS | \DEBUG_BACKTRACE_PROVIDE_OBJECT, 2)[1];
if (($t['object'] ?? null) instanceof self && $t['function'] === "__clone") {
if (($t['object'] ?? null) instanceof Context && $t['function'] === "__clone") {
$this->parent = $t['object'];
}
}
@ -62,209 +27,4 @@ class ExclusionContext {
public function __destruct() {
unset($this->parent);
}
protected function act(string $prop, int $set, $value) {
if ($set) {
if (is_null($value)) {
unset($this->props[$prop]);
$this->$prop = (new \ReflectionClass($this))->getDefaultProperties()[$prop];
} else {
$this->props[$prop] = true;
$this->$prop = $value;
}
return $this->parent ?? $this;
} else {
return isset($this->props[$prop]);
}
}
protected function cleanIdArray(array $spec, bool $allowZero = false): array {
$spec = array_values($spec);
for ($a = 0; $a < sizeof($spec); $a++) {
if (ValueInfo::id($spec[$a], $allowZero)) {
$spec[$a] = (int) $spec[$a];
} else {
$spec[$a] = null;
}
}
return array_values(array_unique(array_filter($spec, function($v) {
return !is_null($v);
})));
}
protected function cleanStringArray(array $spec): array {
$spec = array_values($spec);
$stop = sizeof($spec);
for ($a = 0; $a < $stop; $a++) {
if (strlen($str = ValueInfo::normalize($spec[$a], ValueInfo::T_STRING | ValueInfo::M_DROP) ?? "")) {
$spec[$a] = $str;
} else {
unset($spec[$a]);
}
}
return array_values(array_unique($spec));
}
public function folder(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function folders(array $spec = null) {
if (isset($spec)) {
$spec = $this->cleanIdArray($spec, true);
}
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function folderShallow(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function foldersShallow(array $spec = null) {
if (isset($spec)) {
$spec = $this->cleanIdArray($spec, true);
}
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function tag(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function tags(array $spec = null) {
if (isset($spec)) {
$spec = $this->cleanIdArray($spec);
}
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function tagName(string $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function tagNames(array $spec = null) {
if (isset($spec)) {
$spec = $this->cleanStringArray($spec);
}
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function subscription(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function subscriptions(array $spec = null) {
if (isset($spec)) {
$spec = $this->cleanIdArray($spec);
}
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function edition(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function article(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function editions(array $spec = null) {
if (isset($spec)) {
$spec = $this->cleanIdArray($spec);
}
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function articles(array $spec = null) {
if (isset($spec)) {
$spec = $this->cleanIdArray($spec);
}
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function label(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function labels(array $spec = null) {
if (isset($spec)) {
$spec = $this->cleanIdArray($spec);
}
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function labelName(string $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function labelNames(array $spec = null) {
if (isset($spec)) {
$spec = $this->cleanStringArray($spec);
}
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function annotationTerms(array $spec = null) {
if (isset($spec)) {
$spec = $this->cleanStringArray($spec);
}
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function searchTerms(array $spec = null) {
if (isset($spec)) {
$spec = $this->cleanStringArray($spec);
}
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function titleTerms(array $spec = null) {
if (isset($spec)) {
$spec = $this->cleanStringArray($spec);
}
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function authorTerms(array $spec = null) {
if (isset($spec)) {
$spec = $this->cleanStringArray($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 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);
}
}

262
lib/Context/ExclusionMembers.php

@ -0,0 +1,262 @@
<?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\ValueInfo;
use JKingWeb\Arsse\Misc\Date;
trait ExclusionMembers {
public $folder = null;
public $folders = [];
public $folderShallow = null;
public $foldersShallow = [];
public $tag = null;
public $tags = [];
public $tagName = null;
public $tagNames = [];
public $subscription = null;
public $subscriptions = [];
public $edition = null;
public $editions = [];
public $article = null;
public $articles = [];
public $label = null;
public $labels = [];
public $labelName = null;
public $labelNames = [];
public $annotationTerms = [];
public $searchTerms = [];
public $titleTerms = [];
public $authorTerms = [];
public $articleRange = [null, null];
public $editionRange = [null, null];
public $modifiedRange = [null, null];
public $modifiedRanges = [];
public $markedRange = [null, null];
public $markedRanges = [];
protected function cleanIdArray(array $spec, bool $allowZero = false): array {
$spec = array_values($spec);
for ($a = 0; $a < sizeof($spec); $a++) {
if (ValueInfo::id($spec[$a], $allowZero)) {
$spec[$a] = (int) $spec[$a];
} else {
$spec[$a] = null;
}
}
return array_values(array_unique(array_filter($spec, function($v) {
return !is_null($v);
})));
}
protected function cleanStringArray(array $spec): array {
$spec = array_values($spec);
$stop = sizeof($spec);
for ($a = 0; $a < $stop; $a++) {
if (strlen($str = ValueInfo::normalize($spec[$a], ValueInfo::T_STRING | ValueInfo::M_DROP) ?? "")) {
$spec[$a] = $str;
} else {
unset($spec[$a]);
}
}
return array_values(array_unique($spec));
}
protected function cleanDateRangeArray(array $spec): array {
$spec = array_values($spec);
$stop = sizeof($spec);
for ($a = 0; $a < $stop; $a++) {
if (!is_array($spec[$a]) || sizeof($spec[$a]) !== 2) {
unset($spec[$a]);
} else {
$spec[$a] = ValueInfo::normalize($spec[$a], ValueInfo::T_DATE | ValueInfo::M_ARRAY | ValueInfo::M_DROP);
if ($spec[$a] === [null, null]) {
unset($spec[$a]);
}
}
}
return array_values(array_unique($spec, \SORT_REGULAR));
}
public function folder(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function folders(array $spec = null) {
if (isset($spec)) {
$spec = $this->cleanIdArray($spec, true);
}
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function folderShallow(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function foldersShallow(array $spec = null) {
if (isset($spec)) {
$spec = $this->cleanIdArray($spec, true);
}
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function tag(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function tags(array $spec = null) {
if (isset($spec)) {
$spec = $this->cleanIdArray($spec);
}
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function tagName(string $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function tagNames(array $spec = null) {
if (isset($spec)) {
$spec = $this->cleanStringArray($spec);
}
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function subscription(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function subscriptions(array $spec = null) {
if (isset($spec)) {
$spec = $this->cleanIdArray($spec);
}
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function edition(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function article(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function editions(array $spec = null) {
if (isset($spec)) {
$spec = $this->cleanIdArray($spec);
}
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function articles(array $spec = null) {
if (isset($spec)) {
$spec = $this->cleanIdArray($spec);
}
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function label(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function labels(array $spec = null) {
if (isset($spec)) {
$spec = $this->cleanIdArray($spec);
}
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function labelName(string $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function labelNames(array $spec = null) {
if (isset($spec)) {
$spec = $this->cleanStringArray($spec);
}
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function annotationTerms(array $spec = null) {
if (isset($spec)) {
$spec = $this->cleanStringArray($spec);
}
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function searchTerms(array $spec = null) {
if (isset($spec)) {
$spec = $this->cleanStringArray($spec);
}
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function titleTerms(array $spec = null) {
if (isset($spec)) {
$spec = $this->cleanStringArray($spec);
}
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function authorTerms(array $spec = null) {
if (isset($spec)) {
$spec = $this->cleanStringArray($spec);
}
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function articleRange(?int $start = null, ?int $end = null) {
if ($start === null && $end === null) {
$spec = null;
} else {
$spec = [$start, $end];
}
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function editionRange(?int $start = null, ?int $end = null) {
if ($start === null && $end === null) {
$spec = null;
} else {
$spec = [$start, $end];
}
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function modifiedRange($start = null, $end = null) {
if ($start === null && $end === null) {
$spec = null;
} else {
$spec = [Date::normalize($start), Date::normalize($end)];
}
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function modifiedRanges(array $spec = null) {
if (isset($spec)) {
$spec = $this->cleanDateRangeArray($spec);
}
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function markedRange($start = null, $end = null) {
if ($start === null && $end === null) {
$spec = null;
} else {
$spec = [Date::normalize($start), Date::normalize($end)];
}
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function markedRanges(array $spec = null) {
if (isset($spec)) {
$spec = $this->cleanDateRangeArray($spec);
}
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
}

20
lib/Context/RootContext.php

@ -0,0 +1,20 @@
<?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;
abstract class RootContext extends AbstractContext {
public $limit = 0;
public $offset = 0;
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);
}
}

50
lib/Context/UnionContext.php

@ -0,0 +1,50 @@
<?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;
class UnionContext extends RootContext implements \ArrayAccess, \Countable, \IteratorAggregate {
protected $contexts = [];
#[\ReturnTypeWillChange]
public function offsetExists($offset) {
return isset($this->contexts[$offset]);
}
#[\ReturnTypeWillChange]
public function offsetGet($offset) {
return $this->contexts[$offset] ?? null;
}
#[\ReturnTypeWillChange]
public function offsetSet($offset, $value) {
assert($value instanceof RootContext, new \Exception("Union contexts may only contain other non-exclusion contexts"));
if (isset($offset)) {
$this->contexts[$offset] = $value;
} else {
$this->contexts[] = $value;
}
}
#[\ReturnTypeWillChange]
public function offsetUnset($offset) {
unset($this->contexts[$offset]);
}
public function count(): int {
return count($this->contexts);
}
public function getIterator(): \Traversable {
foreach ($this->contexts as $k => $c) {
yield $k => $c;
}
}
public function __construct(RootContext ...$context) {
$this->contexts = $context;
}
}

618
lib/Database.php