Compare commits

...

14 Commits

Author SHA1 Message Date
J. King d9b90390e7 Update style rules 3 months ago
J. King 59a9329032 Upgrade dependencies where possible with PHP 7.3 6 months ago
J. King be3adf7026 Document RoboFile better 6 months ago
J. King eb371b75fe Fix documentation errors 6 months ago
J. King 1b80ad37bc Merge branch 'csfixer3' 6 months ago
J. King 3c83fc9139 Update php-cs-fixer rules 6 months ago
J. King 711f87aad8 Housekeeping 6 months ago
J. King 0a8d19d37d Require PHP 7.3 8 months ago
J. King fe06ffc176 Avoid dynamic property creation with PicoFeed 8 months ago
J. King 0d6f8d2921 Avoid most deprecation warnings 8 months ago
J. King 92b1a840a1 Support PHP 8.2 properly 8 months ago
J. King 4080b2d09d Apply new rules 2 years ago
J. King 73731fa9db Fix up CS config file 2 years ago
J. King 18d296dcd6 Clean up CS fixer rules 2 years ago
  1. 1
      .gitignore
  2. 33
      .php-cs-fixer.dist.php
  3. 12
      CHANGELOG
  4. 2
      README.md
  5. 84
      RoboFile.php
  6. 6
      UPGRADING
  7. 3
      arsse.php
  8. 28
      composer.json
  9. 625
      composer.lock
  10. 8
      dist/arch/PKGBUILD
  11. 8
      dist/arch/PKGBUILD-git
  12. 2
      dist/debian/control
  13. 2
      docs/en/020_Getting_Started/020_Download_and_Installation/999_ On_Other_Systems.md
  14. 1
      lib/AbstractException.php
  15. 3
      lib/Arsse.php
  16. 1
      lib/CLI.php
  17. 3
      lib/Conf.php
  18. 1
      lib/Conf/Exception.php
  19. 1
      lib/Context/AbstractContext.php
  20. 1
      lib/Context/BooleanMembers.php
  21. 1
      lib/Context/Context.php
  22. 1
      lib/Context/ExclusionContext.php
  23. 2
      lib/Context/ExclusionMembers.php
  24. 1
      lib/Context/RootContext.php
  25. 1
      lib/Context/UnionContext.php
  26. 49
      lib/Database.php
  27. 1
      lib/Db/AbstractDriver.php
  28. 1
      lib/Db/AbstractResult.php
  29. 1
      lib/Db/AbstractStatement.php
  30. 1
      lib/Db/Driver.php
  31. 1
      lib/Db/Exception.php
  32. 1
      lib/Db/ExceptionInput.php
  33. 1
      lib/Db/ExceptionRetry.php
  34. 1
      lib/Db/ExceptionTimeout.php
  35. 1
      lib/Db/MySQL/Driver.php
  36. 5
      lib/Db/MySQL/ExceptionBuilder.php
  37. 1
      lib/Db/MySQL/PDODriver.php
  38. 1
      lib/Db/MySQL/PDOStatement.php
  39. 1
      lib/Db/MySQL/Result.php
  40. 1
      lib/Db/MySQL/Statement.php
  41. 1
      lib/Db/PDODriver.php
  42. 1
      lib/Db/PDOError.php
  43. 1
      lib/Db/PDOResult.php
  44. 1
      lib/Db/PDOStatement.php
  45. 1
      lib/Db/PostgreSQL/Dispatch.php
  46. 1
      lib/Db/PostgreSQL/Driver.php
  47. 1
      lib/Db/PostgreSQL/PDODriver.php
  48. 4
      lib/Db/PostgreSQL/PDOResult.php
  49. 1
      lib/Db/PostgreSQL/PDOStatement.php
  50. 1
      lib/Db/PostgreSQL/Result.php
  51. 1
      lib/Db/PostgreSQL/Statement.php
  52. 1
      lib/Db/Result.php
  53. 1
      lib/Db/ResultAggregate.php
  54. 1
      lib/Db/ResultEmpty.php
  55. 1
      lib/Db/SQLState.php
  56. 1
      lib/Db/SQLite3/AbstractPDODriver.php
  57. 2
      lib/Db/SQLite3/Driver.php
  58. 1
      lib/Db/SQLite3/ExceptionBuilder.php
  59. 1
      lib/Db/SQLite3/PDODriver.php
  60. 1
      lib/Db/SQLite3/PDOStatement.php
  61. 1
      lib/Db/SQLite3/Result.php
  62. 1
      lib/Db/SQLite3/Statement.php
  63. 1
      lib/Db/Statement.php
  64. 1
      lib/Db/Transaction.php
  65. 1
      lib/Exception.php
  66. 1
      lib/ExceptionFatal.php
  67. 1
      lib/ExceptionType.php
  68. 1
      lib/Factory.php
  69. 106
      lib/Feed.php
  70. 1
      lib/Feed/Exception.php
  71. 25
      lib/Feed/Item.php
  72. 1
      lib/ImportExport/AbstractImportExport.php
  73. 1
      lib/ImportExport/Exception.php
  74. 1
      lib/ImportExport/OPML.php
  75. 1
      lib/Lang.php
  76. 1
      lib/Lang/Exception.php
  77. 1
      lib/Misc/Date.php
  78. 1
      lib/Misc/HTTP.php
  79. 1
      lib/Misc/Query.php
  80. 1
      lib/Misc/QueryFilter.php
  81. 4
      lib/Misc/URL.php
  82. 17
      lib/Misc/ValueInfo.php
  83. 1
      lib/REST.php
  84. 1
      lib/REST/AbstractHandler.php
  85. 1
      lib/REST/Exception.php
  86. 1
      lib/REST/Exception501.php
  87. 1
      lib/REST/Fever/API.php
  88. 1
      lib/REST/Fever/User.php
  89. 1
      lib/REST/Handler.php
  90. 1
      lib/REST/Miniflux/Status.php
  91. 1
      lib/REST/Miniflux/Token.php
  92. 3
      lib/REST/Miniflux/V1.php
  93. 9
      lib/REST/NextcloudNews/V1_2.php
  94. 1
      lib/REST/NextcloudNews/Versions.php
  95. 3
      lib/REST/TinyTinyRSS/API.php
  96. 1
      lib/REST/TinyTinyRSS/Exception.php
  97. 1
      lib/REST/TinyTinyRSS/Icon.php
  98. 3
      lib/REST/TinyTinyRSS/Search.php
  99. 1
      lib/Rule/Exception.php
  100. 1
      lib/Rule/Rule.php

1
.gitignore

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

33
.php_cs.dist → .php-cs-fixer.dist.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse;
const BASE = __DIR__.DIRECTORY_SEPARATOR;
@ -13,7 +14,10 @@ $paths = [
BASE."arsse.php",
BASE."RoboFile.php",
BASE."lib",
BASE."tests",
BASE."tests/cases",
BASE."tests/lib",
BASE."tests/bootstrap.php",
BASE."tests/server.php",
];
$rules = [
// house rules where PSR series is silent
@ -35,6 +39,7 @@ $rules = [
'no_blank_lines_after_phpdoc' => true,
'no_empty_comment' => true,
'no_empty_phpdoc' => true,
'no_empty_statement' => true,
'no_extra_blank_lines' => true, // this could probably use more configuration
'no_mixed_echo_print' => ['use' => "echo"],
'no_short_bool_cast' => true,
@ -48,28 +53,16 @@ $rules = [
'pow_to_exponentiation' => true,
'set_type_to_cast' => true,
'standardize_not_equals' => true,
'trailing_comma_in_multiline_array' => true,
'trailing_comma_in_multiline' => ['elements' => ["arrays"]],
'unary_operator_spaces' => true,
'yoda_style' => false,
// PSR standard to apply
'@PSR2' => true,
// PSR-12 rules; php-cs-fixer does not yet support PSR-12 natively
'compact_nullable_typehint' => true,
'declare_equal_normalize' => ['space' => "none"],
'function_typehint_space' => true,
'lowercase_cast' => true,
'lowercase_static_reference' => true,
'no_alternative_syntax' => true,
'no_empty_statement' => true,
'no_leading_import_slash' => true,
'no_leading_namespace_whitespace' => true,
'no_whitespace_in_blank_line' => true,
'return_type_declaration' => ['space_before' => "none"],
'single_trait_insert_per_statement' => true,
'short_scalar_cast' => true,
'visibility_required' => ['elements' => ["const", "property", "method"]],
'@PSR12' => true,
// house exceptions to PSR rules
'braces' => ['position_after_functions_and_oop_constructs' => "same"],
'curly_braces_position' => [
'functions_opening_brace' => "same_line",
'classes_opening_brace' => "same_line",
],
'function_declaration' => ['closure_function_spacing' => "none"],
'new_with_braces' => false, // no option to specify absence of braces
];
@ -82,4 +75,4 @@ foreach ($paths as $path) {
$finder = $finder->in($path);
}
}
return \PhpCsFixer\Config::create()->setRiskyAllowed(true)->setRules($rules)->setFinder($finder);
return (new \PhpCsFixer\Config)->setRiskyAllowed(true)->setRules($rules)->setFinder($finder);

12
CHANGELOG

@ -1,3 +1,15 @@
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)
===========================

2
README.md

@ -122,5 +122,3 @@ Producing release packages is done by running `./robo package`. This performs th
- Deletes numerous unneeded files
- Exports the default configuration of The Arsse to a file
- Compresses the remaining files into a tarball
- Produces a binary package for Arch Linux, if possible
- Produces source and binary packages for Debian using [pbuilder](https://pbuilder-team.pages.debian.net/pbuilder/), if possible

84
RoboFile.php

@ -55,8 +55,8 @@ class RoboFile extends \Robo\Tasks {
* tests/coverage/. Additional reports may be produced by passing
* arguments to this task as one would to PHPUnit.
*
* Robo first tries to use pcov and will fall back first to xdebug then
* phpdbg. Neither pcov nor xdebug need to be enabled to be used; they
* Robo first tries to use pcov and will fall back to xdebug.
* Neither pcov nor xdebug need to be enabled to be used; they
* only need to be present in the extension load path to be used.
*/
public function coverage(array $args): Result {
@ -80,9 +80,9 @@ class RoboFile extends \Robo\Tasks {
return $this->runTests($exec, "typical", array_merge(["--coverage-html", BASE_TEST."coverage"], $args));
}
/** Runs the coding standards fixer */
/** Runs the coding-style fixer */
public function clean($opts = ['demo|d' => false]): Result {
$t = $this->taskExec(norm(BASE."vendor/bin/php-cs-fixer"));
$t = $this->taskExec(norm(BASE."vendor-bin/csfixer/vendor/bin/php-cs-fixer"));
$t->arg("fix");
if ($opts['demo']) {
$t->args("--dry-run", "--diff")->option("--diff-format", "udiff");
@ -90,9 +90,10 @@ class RoboFile extends \Robo\Tasks {
return $t->run();
}
/** Finds the first suitable means of computing code coverage, either pcov or xdebug. */
protected function findCoverageEngine(): string {
$dir = rtrim(ini_get("extension_dir"), "/").\DIRECTORY_SEPARATOR;
$ext = IS_WIN ? "dll" : (IS_MAC ? "dylib" : "so");
$ext = IS_WIN ? "dll" : "so";
$php = escapeshellarg(\PHP_BINARY);
$code = escapeshellarg(BASE."lib");
if (extension_loaded("pcov")) {
@ -104,25 +105,27 @@ class RoboFile extends \Robo\Tasks {
} elseif (file_exists($dir."xdebug.$ext")) {
return "$php -d zend_extension=xdebug.$ext -d xdebug.mode=coverage";
} else {
if (IS_WIN) {
$dbg = dirname(\PHP_BINARY)."\\phpdbg.exe";
$dbg = file_exists($dbg) ? $dbg : "";
} else {
$dbg = trim(`which phpdbg 2>/dev/null`);
}
if ($dbg) {
return escapeshellarg($dbg)." -qrr";
} else {
return $php;
}
return $php;
}
}
/** Returns the necessary shell arguments to print error output or all output to the bitbucket
*
* @param bool $all Whether all output (true) or only error output (false) should be suppressed
*/
protected function blackhole(bool $all = false): string {
$hole = IS_WIN ? "nul" : "/dev/null";
return $all ? ">$hole 2>&1" : "2>$hole";
}
/** Executes PHPUnit, used by the test and coverage tasks.
*
* This also executes the built-in PHP Web server, which is required to fetch some newsfeeds during tests
*
* @param string $executor The path to the PHP binary to execute with any required extra arguments. Normally this is either "php" or the result of findCoverageEngine()
* @param string $set The set of tests to run, either "typical" (excludes redundant tests), "quick" (excludes redundant and slow tests), "coverage" (excludes tests not needed for coverage), or "full" (all tests)
* @param array $args Extra arguments passed by Robo from the command line
*/
protected function runTests(string $executor, string $set, array $args): Result {
switch ($set) {
case "typical":
@ -146,6 +149,12 @@ class RoboFile extends \Robo\Tasks {
return $this->taskExec($executor)->option("-d", "zend.assertions=1")->arg($execpath)->option("-c", $confpath)->args(array_merge($set, $args))->run();
}
/** Returns a Git version string for a given Git tree-ish ID
*
* Returns an array containing the tree-ish string and the version string.
*
* @param string|null $commit The tree-ish ID. If not supplied the user will be prompted
*/
protected function commitVersion(?string $commit): array {
$target = $commit ?? $this->askDefault("Reference commit:", "HEAD");
$base = escapeshellarg(BASE);
@ -158,13 +167,11 @@ class RoboFile extends \Robo\Tasks {
return [$target, $version];
}
/** Checks whether all the supplied terminal commands are available in path */
protected function toolExists(string ...$binary): bool {
$blackhole = $this->blackhole(IS_WIN);
foreach ($binary as $bin) {
if (
(IS_WIN && (!exec(escapeshellarg($bin)." --help $blackhole", $junk, $status) || $status))
|| (!IS_WIN && (!exec("which ".escapeshellarg($bin)." $blackhole", $junk, $status) || $status))
) {
if (!exec(escapeshellarg($bin)." --help $blackhole", $junk, $status) || $status) {
return false;
}
}
@ -400,12 +407,12 @@ class RoboFile extends \Robo\Tasks {
return $t->run();
}
/** Generates static manual pages in the "manual" directory
/** Generates static HTML manual pages in the "manual" directory
*
* The resultant files are suitable for offline viewing and inclusion into release builds
*/
public function manual(array $args): Result {
$execpath = escapeshellarg(norm(BASE."vendor/bin/daux"));
$execpath = escapeshellarg(norm(BASE."vendor-bin/daux/vendor/bin/daux"));
$t = $this->collectionBuilder();
$t->taskExec($execpath)->arg("generate")->option("-d", BASE."manual")->args($args);
$t->taskDeleteDir(BASE."manual/daux_libraries");
@ -416,7 +423,7 @@ class RoboFile extends \Robo\Tasks {
/** Serves a live view of the manual using the built-in Web server */
public function manualLive(array $args): Result {
$execpath = escapeshellarg(norm(BASE."vendor/bin/daux"));
$execpath = escapeshellarg(norm(BASE."vendor-bin/daux/vendor/bin/daux"));
return $this->taskExec($execpath)->arg("serve")->args($args)->run();
}
@ -470,6 +477,15 @@ class RoboFile extends \Robo\Tasks {
return $t->run();
}
/** Parses the contents of the CHANGELOG file into an array structure
*
* This is done line-by-line and tends to be quite strict.
* The parsed output can be used to generate changelogs in other formats,
* such as a Debian changelog or RPM changelog.
*
* @param string $text The text of the CHANGELOG file
* @param string $targetVersion The x.y.z version number of the latest release. This is used to check that version numbers and dates have been updated when preparing a release
*/
protected function changelogParse(string $text, string $targetVersion): array {
$lines = preg_split('/\r?\n/', $text);
$version = "";
@ -546,7 +562,18 @@ class RoboFile extends \Robo\Tasks {
return $out;
}
/** Produce a Debian changelog from a parsed CHANGELOG file
*
* The Debian changelog format is extremely specific with certain tokens
* having special meaning and leading whitespace also being significant.
* Modifying this function should be done with extreme care.
*
* @param array $log The parsed chaneglog, output by changelogParse()
* @param string $targetVersion The second output of commitVersion()
*/
protected function changelogDebian(array $log, string $targetVersion): string {
$authorName = "J. King";
$authorMail = "jking@jkingweb.ca";
$latest = $log[0]['version'];
$baseVersion = preg_replace('/^(\d+(?:\.\d+)*).*/', "$1", $targetVersion);
if ($baseVersion !== $targetVersion && version_compare($latest, $baseVersion, ">")) {
@ -582,11 +609,20 @@ class RoboFile extends \Robo\Tasks {
$out .= " * ".trim(preg_replace("/^/m", " ", $item))."\n";
}
}
$out .= "\n -- J. King <jking@jkingweb.ca> ".\DateTimeImmutable::createFromFormat("Y-m-d", $entry['date'], new \DateTimeZone("UTC"))->format("D, d M Y")." 00:00:00 +0000\n\n";
$out .= "\n -- $authorName <$authorMail> ".\DateTimeImmutable::createFromFormat("Y-m-d", $entry['date'], new \DateTimeZone("UTC"))->format("D, d M Y")." 00:00:00 +0000\n\n";
}
return $out;
}
/** Produces a Debian "source control" file from various bits of data
*
* As with a Debian changelog, the output is of a very exacting format,
* and this function should be modified with care.
*
* @param string $dir The path to Debian-specific files, with trailing slash
* @param string $version The Debian version string, in the format x.y.z-a
* @param array $tarballs An array of paths to the "orig" and "debian" tarball files
*/
protected function generateDebianSourceControl(string $dir, string $version, array $tarballs): string {
// read in control file
if (!$control = @file_get_contents($dir."control")) {

6
UPGRADING

@ -11,6 +11,12 @@ usually prudent:
`composer install -o --no-dev`
Upgrading from 0.10.4 to 0.10.?
=============================
- PHP 7.3 is now required
Upgrading from 0.10.2 to 0.10.3
=============================

3
arsse.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse;
const BASE = __DIR__.DIRECTORY_SEPARATOR;
@ -13,7 +14,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 because various dependencies have yet to adjust to PHP 8.1
// 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") {

28
composer.json

@ -18,36 +18,38 @@
],
"require": {
"php": "^7.1 || ^8.0",
"php": ">=7.3",
"ext-intl": "*",
"ext-json": "*",
"ext-hash": "*",
"ext-filter": "*",
"ext-dom": "*",
"nicolus/picofeed": "^0.1.43",
"nicolus/picofeed": "dev-patch-3",
"hosteurope/password-generator": "1.*",
"docopt/docopt": "1.*",
"jkingweb/druuid": "3.*",
"guzzlehttp/psr7": "1.*",
"laminas/laminas-httphandlerrunner": "1.*"
"guzzlehttp/psr7": "2.*",
"laminas/laminas-httphandlerrunner": "2.*"
},
"require-dev": {
"bamarni/composer-bin-plugin": "*"
},
"suggest": {
"ext-pcntl": "To respond to signals, particular to reload configuration via SIGHUP"
"ext-pcntl": "To respond to signals, particularly to reload configuration via SIGHUP"
},
"config": {
"platform": {
"php": "7.1.33"
"php": "7.3.33"
},
"allow-plugins": {
"bamarni/composer-bin-plugin": true
}
},
"scripts": {
"post-install-cmd": ["@composer bin all install"],
"post-update-cmd": ["@composer bin all update"]
"extra": {
"bamarni-bin": {
"bin-links": false,
"forward-command": true
}
},
"autoload": {
"psr-4": {
@ -59,5 +61,11 @@
"JKingWeb\\Arsse\\Test\\": "tests/lib/",
"JKingWeb\\Arsse\\TestCase\\": "tests/cases/"
}
}
},
"repositories": [
{
"type": "vcs",
"url": "https://github.com/JKingweb/picoFeed-1/"
}
]
}

625
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"

8
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")
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:

1
lib/AbstractException.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse;
abstract class AbstractException extends \Exception {

3
lib/Arsse.php

@ -4,10 +4,11 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse;
class Arsse {
public const VERSION = "0.10.3";
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",

1
lib/CLI.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse;
use JKingWeb\Arsse\REST\Fever\User as Fever;

3
lib/Conf.php

@ -5,14 +5,17 @@
/** Conf class */
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. */
#[AllowDynamicProperties]
class Conf {
/** @var string Default language to use for logging and errors */
public $lang = "en";

1
lib/Conf/Exception.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Conf;
class Exception extends \JKingWeb\Arsse\AbstractException {

1
lib/Context/AbstractContext.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Context;
abstract class AbstractContext {

1
lib/Context/BooleanMembers.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Context;
trait BooleanMembers {

1
lib/Context/Context.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Context;
class Context extends RootContext {

1
lib/Context/ExclusionContext.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Context;
class ExclusionContext extends AbstractContext {

2
lib/Context/ExclusionMembers.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Context;
use JKingWeb\Arsse\Misc\ValueInfo;
@ -243,7 +244,6 @@ trait ExclusionMembers {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function markedRange($start = null, $end = null) {
if ($start === null && $end === null) {
$spec = null;

1
lib/Context/RootContext.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Context;
abstract class RootContext extends AbstractContext {

1
lib/Context/UnionContext.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Context;
class UnionContext extends RootContext implements \ArrayAccess, \Countable, \IteratorAggregate {

49
lib/Database.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse;
use JKingWeb\DrUUID\UUID;
@ -280,7 +281,7 @@ class Database {
}
/** Renames a user
*
*
* This does not have an effect on their numeric ID, but has a cascading effect on many tables
*/
public function userRename(string $user, string $name): bool {
@ -337,7 +338,7 @@ class Database {
}
/** Retrieves any metadata associated with a user
*
*
* @param string $user The user whose metadata is to be retrieved
* @param bool $includeLarge Whether to include values which can be arbitrarily large text
*/
@ -855,8 +856,8 @@ class Database {
sum(case when \"read\" = 1 and hidden = 0 then 1 else 0 end) as marked
from arsse_marks group by subscription
) as mark_stats on mark_stats.subscription = s.id",
["str", "int"],
[$user, $folder]
["str", "int"],
[$user, $folder]
);
$q->setWhere("s.owner = ?", ["str"], [$user]);
$nocase = $this->db->sqlToken("nocase");
@ -1335,12 +1336,12 @@ class Database {
"UPDATE arsse_feeds SET title = ?, source = ?, updated = CURRENT_TIMESTAMP, modified = ?, etag = ?, err_count = 0, err_msg = '', next_fetch = ?, size = ?, icon = ? WHERE id = ?",
["str", "str", "datetime", "strict str", "datetime", "int", "int", "int"]
)->run(
$feed->data->title,
$feed->data->siteUrl,
$feed->title,
$feed->siteUrl,
$feed->lastModified,
$feed->resource->getEtag(),
$feed->etag,
$feed->nextFetch,
sizeof($feed->data->items),
sizeof($feed->items),
$icon,
$feedID
);
@ -1613,26 +1614,26 @@ class Database {
}
// ensure any used array-type context options contain at least one member
foreach ([
"articles",
"articles",
"editions",
"subscriptions",
"folders",
"foldersShallow",
"labels",
"labelNames",
"tags",
"tagNames",
"searchTerms",
"titleTerms",
"authorTerms",
"folders",
"foldersShallow",
"labels",
"labelNames",
"tags",
"tagNames",
"searchTerms",
"titleTerms",
"authorTerms",
"annotationTerms",
"modifiedRanges",
"markedRanges",
] as $m) {
if ($context->$m() && !$context->$m) {
throw new Db\ExceptionInput("tooShort", ['field' => $m, 'action' => $this->caller(), 'min' => 1]);
}
if ($context->$m() && !$context->$m) {
throw new Db\ExceptionInput("tooShort", ['field' => $m, 'action' => $this->caller(), 'min' => 1]);
}
}
// next compute the context, supplying the query to manipulate directly
$this->articleFilter($context, $q);
}
@ -1921,8 +1922,8 @@ class Database {
touched = 1
where
article in (select article from target_articles)
and subscription in (select distinct subscription from target_articles)",
[$subq->getTypes(), "bool"],
and subscription in (select distinct subscription from target_articles)",
[$subq->getTypes(), "bool"],
[$subq->getValues(), $data['read']]
);
$this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues());
@ -1952,7 +1953,7 @@ class Database {
where
article in (select article from target_articles)
and subscription in (select distinct subscription from target_articles)",
[$subq->getTypes(), $setTypes],
[$subq->getTypes(), $setTypes],
[$subq->getValues(), $setValues]
);
$this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues());

1
lib/Db/AbstractDriver.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Db;
use JKingWeb\Arsse\Arsse;

1
lib/Db/AbstractResult.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Db;
abstract class AbstractResult implements Result {

1
lib/Db/AbstractStatement.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Db;
use JKingWeb\Arsse\Misc\Date;

1
lib/Db/Driver.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Db;
interface Driver {

1
lib/Db/Exception.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Db;
class Exception extends \JKingWeb\Arsse\AbstractException {

1
lib/Db/ExceptionInput.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Db;
class ExceptionInput extends \JKingWeb\Arsse\AbstractException {

1
lib/Db/ExceptionRetry.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Db;
class ExceptionRetry extends \JKingWeb\Arsse\AbstractException {

1
lib/Db/ExceptionTimeout.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Db;
class ExceptionTimeout extends \JKingWeb\Arsse\AbstractException {

1
lib/Db/MySQL/Driver.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Db\MySQL;
use JKingWeb\Arsse\Arsse;

5
lib/Db/MySQL/ExceptionBuilder.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Db\MySQL;
use JKingWeb\Arsse\Db\Exception;
@ -27,7 +28,7 @@ trait ExceptionBuilder {
public static function buildConnectionException($code, string $msg): array {
switch ($code) {
case 1045:
// @codeCoverageIgnoreStart
// @codeCoverageIgnoreStart
case 1043:
case 1044:
case 1046:
@ -48,7 +49,7 @@ trait ExceptionBuilder {
case 2018:
case 2026:
case 2028:
// @codeCoverageIgnoreEnd
// @codeCoverageIgnoreEnd
return [Exception::class, 'connectionFailure', ['engine' => "MySQL", 'message' => $msg]];
default:
return [Exception::class, 'engineErrorGeneral', $msg]; // @codeCoverageIgnore

1
lib/Db/MySQL/PDODriver.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Db\MySQL;
use JKingWeb\Arsse\Arsse;

1
lib/Db/MySQL/PDOStatement.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Db\MySQL;
class PDOStatement extends \JKingWeb\Arsse\Db\PDOStatement {

1
lib/Db/MySQL/Result.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Db\MySQL;
class Result extends \JKingWeb\Arsse\Db\AbstractResult {

1
lib/Db/MySQL/Statement.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Db\MySQL;
class Statement extends \JKingWeb\Arsse\Db\AbstractStatement {

1
lib/Db/PDODriver.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Db;
trait PDODriver {

1
lib/Db/PDOError.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Db;
trait PDOError {

1
lib/Db/PDOResult.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Db;
class PDOResult extends AbstractResult {

1
lib/Db/PDOStatement.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Db;
abstract class PDOStatement extends AbstractStatement {

1
lib/Db/PostgreSQL/Dispatch.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Db\PostgreSQL;
trait Dispatch {

1
lib/Db/PostgreSQL/Driver.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Db\PostgreSQL;
use JKingWeb\Arsse\Arsse;

1
lib/Db/PostgreSQL/PDODriver.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Db\PostgreSQL;
use JKingWeb\Arsse\Arsse;

4
lib/Db/PostgreSQL/PDOResult.php

@ -4,11 +4,11 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Db\PostgreSQL;
class PDOResult extends \JKingWeb\Arsse\Db\PDOResult {
// This method exists to transparent handle byte-array results
// This method exists to transparently handle byte-array results
public function valid() {
$this->cur = $this->set->fetch(\PDO::FETCH_ASSOC);

1
lib/Db/PostgreSQL/PDOStatement.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Db\PostgreSQL;
use JKingWeb\Arsse\Db\Result;

1
lib/Db/PostgreSQL/Result.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Db\PostgreSQL;
class Result extends \JKingWeb\Arsse\Db\AbstractResult {

1
lib/Db/PostgreSQL/Statement.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Db\PostgreSQL;
class Statement extends \JKingWeb\Arsse\Db\AbstractStatement {

1
lib/Db/Result.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Db;
interface Result extends \Iterator {

1
lib/Db/ResultAggregate.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Db;
class ResultAggregate extends AbstractResult {

1
lib/Db/ResultEmpty.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Db;
class ResultEmpty extends AbstractResult {

1
lib/Db/SQLState.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Db;
trait SQLState {

1
lib/Db/SQLite3/AbstractPDODriver.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Db\SQLite3;
abstract class AbstractPDODriver extends Driver {

2
lib/Db/SQLite3/Driver.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Db\SQLite3;
use JKingWeb\Arsse\Arsse;
@ -20,6 +21,7 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
public const SQLITE_MISMATCH = 20;
protected $db;
protected $collator;
public function __construct() {
// check to make sure required extension is loaded

1
lib/Db/SQLite3/ExceptionBuilder.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Db\SQLite3;
use JKingWeb\Arsse\Db\Exception;

1
lib/Db/SQLite3/PDODriver.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Db\SQLite3;
use JKingWeb\Arsse\Arsse;

1
lib/Db/SQLite3/PDOStatement.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Db\SQLite3;
class PDOStatement extends \JKingWeb\Arsse\Db\PDOStatement {

1
lib/Db/SQLite3/Result.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Db\SQLite3;
class Result extends \JKingWeb\Arsse\Db\AbstractResult {

1
lib/Db/SQLite3/Statement.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Db\SQLite3;
class Statement extends \JKingWeb\Arsse\Db\AbstractStatement {

1
lib/Db/Statement.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Db;
interface Statement {

1
lib/Db/Transaction.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Db;
class Transaction {

1
lib/Exception.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse;
class Exception extends AbstractException {

1
lib/ExceptionFatal.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse;
class ExceptionFatal extends AbstractException {

1
lib/ExceptionType.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse;
class ExceptionType extends AbstractException {

1
lib/Factory.php

@ -4,6 +4,7 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse;
class Factory {

106
lib/Feed.php

@ -4,8 +4,10 @@
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse;
use JKingWeb\Arsse\Feed\Item;
use JKingWeb\Arsse\Misc\Date;
use JKingWeb\Arsse\Rule\Rule;
use PicoFeed\PicoFeedException;
@ -16,62 +18,62 @@ use PicoFeed\Reader\Favicon;
use PicoFeed\Scraper\Scraper;
class Feed {
public $data = null;
public $title;
public $siteUrl;
public $iconUrl;
public $iconType;
public $iconData;
public $resource;
public $modified = false;
public $lastModified;
public $etag;
public $nextFetch;
public $items = [];
public $newItems = [];
public $changedItems = [];
public $filteredItems = [];
public static function discover(string $url, string $username = '', string $password = ''): string {
// fetch the candidate feed
$f = self::download($url, "", "", $username, $password);
if ($f->reader->detectFormat($f->getContent())) {
[$client, $reader] = self::download($url, "", "", $username, $password);
if ($reader->detectFormat($client->getContent())) {
// if the prospective URL is a feed, use it
$out = $url;
} else {
$links = $f->reader->find($f->getUrl(), $f->getContent());
$links = $reader->find($client->getUrl(), $client->getContent());
if (!$links) {
// work around a PicoFeed memory leak
libxml_use_internal_errors(false);
throw new Feed\Exception("", ['url' => $url], new \PicoFeed\Reader\SubscriptionNotFoundException('Unable to find a subscription'));
} else {
$out = $links[0];
}
}
// work around a PicoFeed memory leak
libxml_use_internal_errors(false);
return $out;
}
public static function discoverAll(string $url, string $username = '', string $password = ''): array {
// fetch the candidate feed
$f = self::download($url, "", "", $username, $password);
if ($f->reader->detectFormat($f->getContent())) {
[$client, $reader] = self::download($url, "", "", $username, $password);
if ($reader->detectFormat($client->getContent())) {
// if the prospective URL is a feed, use it
return [$url];
} else {
return $f->reader->find($f->getUrl(), $f->getContent());
return $reader->find($client->getUrl(), $client->getContent());
}
}
public function __construct(int $feedID = null, string $url, string $lastModified = '', string $etag = '', string $username = '', string $password = '', bool $scrape = false) {
// fetch the feed
$this->resource = self::download($url, $lastModified, $etag, $username, $password);
[$client, $reader] = self::download($url, $lastModified, $etag, $username, $password);
// format the HTTP Last-Modified date returned
$lastMod = $this->resource->getLastModified();
$lastMod = $client->getLastModified();
if (strlen($lastMod ?? "")) {
$this->lastModified = Date::normalize($lastMod, "http");
}
$this->modified = $this->resource->isModified();
//parse the feed, if it has been modified
$this->modified = $client->isModified();
// get the ETag
$this->etag = $client->getEtag();
// parse the feed, if it has been modified
if ($this->modified) {
$this->parse();
$this->parse($client, $reader);
// ascertain whether there are any articles not in the database
$this->matchToDatabase($feedID);
// if caching header fields are not sent by the server, try to ascertain a last-modified date from the feed contents
@ -112,12 +114,11 @@ class Feed {
return $config;
}
protected static function download(string $url, string $lastModified, string $etag, string $username, string $password): Client {
protected static function download(string $url, string $lastModified, string $etag, string $username, string $password): array {
try {
$reader = new Reader(self::configure());
$client = $reader->download($url, $lastModified, $etag, $username, $password);
$client->reader = $reader;
return $client;
return [$client, $reader];
} catch (PicoFeedException $e) {
throw new Feed\Exception("", ['url' => $url], $e); // @codeCoverageIgnore
} catch (\GuzzleHttp\Exception\GuzzleException $e) {
@ -125,17 +126,17 @@ class Feed {
}
}
protected function parse(): void {
protected function parse(Client $client, Reader $reader): void {
try {
$feed = $this->resource->reader->getParser(
$this->resource->getUrl(),
$this->resource->getContent(),
$this->resource->getEncoding()
$feed = $reader->getParser(
$client->getUrl(),
$client->getContent(),
$client->getEncoding()
)->execute();
} catch (PicoFeedException $e) {
throw new Feed\Exception("", ['url' => $this->resource->getUrl()], $e);
throw new Feed\Exception("", ['url' => $client->getUrl()], $e);
} catch (\GuzzleHttp\Exception\GuzzleException $e) { // @codeCoverageIgnore
throw new Feed\Exception("", ['url' => $this->resource->getUrl()], $e); // @codeCoverageIgnore
throw new Feed\Exception("", ['url' => $client->getUrl()], $e); // @codeCoverageIgnore
}
// Grab the favicon for the feed, or null if no valid icon is found
@ -150,6 +151,10 @@ class Feed {
$this->iconUrl = $this->iconData = null;
}
// Next gather all other feed-level information we want out of the feed
$this->siteUrl = $feed->siteUrl;
$this->title = $feed->title;
// PicoFeed does not provide valid ids when there is no id element. Its solution
// of hashing the url, title, and content together for the id if there is no id