mirror of
https://code.mensbeam.com/MensBeam/Arsse.git
synced 2024-12-22 21:22:40 +00:00
Merge branch 'master' into microsub
This commit is contained in:
commit
4a0face9af
249 changed files with 13361 additions and 6779 deletions
27
.gitignore
vendored
27
.gitignore
vendored
|
@ -1,16 +1,25 @@
|
||||||
# Temporary files and dependencies
|
# Temporary files
|
||||||
|
|
||||||
|
/release/
|
||||||
|
/documentation/
|
||||||
|
/manual/
|
||||||
|
/tests/coverage/
|
||||||
|
/dist/arch/arsse/
|
||||||
|
/dist/arch/src/
|
||||||
|
/dist/arch/pkg/
|
||||||
|
/dist/man/
|
||||||
|
/arsse.db*
|
||||||
|
/config.php
|
||||||
|
/.php_cs.cache
|
||||||
|
/tests/.phpunit.result.cache
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
|
||||||
/vendor/
|
/vendor/
|
||||||
/vendor-bin/*/vendor
|
/vendor-bin/*/vendor
|
||||||
/node_modules
|
/node_modules
|
||||||
/documentation/
|
/yarn.lock
|
||||||
/manual/
|
|
||||||
/tests/coverage/
|
|
||||||
/arsse.db*
|
|
||||||
/config.php
|
|
||||||
/.php_cs.cache
|
|
||||||
/yarn-error.log
|
/yarn-error.log
|
||||||
/tests/.phpunit.result.cache
|
|
||||||
|
|
||||||
|
|
||||||
# Windows files
|
# Windows files
|
||||||
|
@ -26,7 +35,6 @@ $RECYCLE.BIN/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.AppleDouble
|
.AppleDouble
|
||||||
.LSOverride
|
.LSOverride
|
||||||
Icon
|
|
||||||
._*
|
._*
|
||||||
.Spotlight-V100
|
.Spotlight-V100
|
||||||
.Trashes
|
.Trashes
|
||||||
|
@ -37,6 +45,7 @@ Icon
|
||||||
*.zip
|
*.zip
|
||||||
*.7z
|
*.7z
|
||||||
*.tar.gz
|
*.tar.gz
|
||||||
|
*.tar.xz
|
||||||
*.tgz
|
*.tgz
|
||||||
*.deb
|
*.deb
|
||||||
*.rpm
|
*.rpm
|
||||||
|
|
62
CHANGELOG
62
CHANGELOG
|
@ -1,3 +1,60 @@
|
||||||
|
Version 0.10.0 (2021-07-11)
|
||||||
|
===========================
|
||||||
|
|
||||||
|
New features:
|
||||||
|
- Complete Unix manual page
|
||||||
|
- Support for running service as a forking daemon
|
||||||
|
- Respond to TERM and HUP signals when possible
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
- Packages for Debian and related are now available (see manual for details)
|
||||||
|
|
||||||
|
Version 0.9.2 (2021-05-25)
|
||||||
|
==========================
|
||||||
|
|
||||||
|
Bug fixes:
|
||||||
|
- Do not fail adding users to an empty database (regression since 0.9.0)
|
||||||
|
- Cleanly ignore unknown configuration properties
|
||||||
|
- Set access mode to rw-r---- when creating SQLite databases
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
- Packages for Arch Linux are now available (see manual for details)
|
||||||
|
- Numerous improvements to the manual
|
||||||
|
|
||||||
|
Version 0.9.1 (2021-03-18)
|
||||||
|
==========================
|
||||||
|
|
||||||
|
Bug fixes:
|
||||||
|
- Respond to PUT requests with 201 rather than 200 in Miniflux
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
- Correct Web server configuration in manual
|
||||||
|
|
||||||
|
Version 0.9.0 (2021-03-06)
|
||||||
|
==========================
|
||||||
|
|
||||||
|
New features:
|
||||||
|
- Support for the Miniflux protocol (see manual for details)
|
||||||
|
- Support for API level 15 of Tiny Tiny RSS
|
||||||
|
- Support for feed icons in Fever
|
||||||
|
- Command-line functionality for managing user metadata
|
||||||
|
- Command-line functionality for managing Miniflux login tokens
|
||||||
|
|
||||||
|
Bug fixes:
|
||||||
|
- Further relax Fever HTTP correctness, to fix more clients
|
||||||
|
- Use icons specified in Atom feeds when available
|
||||||
|
- Do not return null as subscription unread count
|
||||||
|
- Explicitly forbid U+003A COLON and control characters in usernames, for
|
||||||
|
compatibility with RFC 7617
|
||||||
|
- Never return 401 in response to an OPTIONS request
|
||||||
|
- Accept "t" and "f" as booleans in Tiny Tiny RSS
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
- Administrator account requirements for Nextcloud News functionality are
|
||||||
|
now enforced
|
||||||
|
- E_DEPRECATED is now suppressed for compatibility with PHP 8 until affected
|
||||||
|
dependencies can be replaced
|
||||||
|
|
||||||
Version 0.8.5 (2020-10-27)
|
Version 0.8.5 (2020-10-27)
|
||||||
==========================
|
==========================
|
||||||
|
|
||||||
|
@ -84,7 +141,7 @@ Bug fixes:
|
||||||
Version 0.6.1 (2019-01-23)
|
Version 0.6.1 (2019-01-23)
|
||||||
==========================
|
==========================
|
||||||
|
|
||||||
Bug Fixes:
|
Bug fixes:
|
||||||
- Unify SQL timeout settings
|
- Unify SQL timeout settings
|
||||||
- Correctly escape shell command in subprocess service driver
|
- Correctly escape shell command in subprocess service driver
|
||||||
- Correctly allow null time intervals in configuration when appropriate
|
- Correctly allow null time intervals in configuration when appropriate
|
||||||
|
@ -204,4 +261,5 @@ Bug fixes:
|
||||||
Version 0.1.0 (2017-08-29)
|
Version 0.1.0 (2017-08-29)
|
||||||
==========================
|
==========================
|
||||||
|
|
||||||
Initial release
|
New features:
|
||||||
|
- Initial release
|
||||||
|
|
27
README.md
27
README.md
|
@ -8,7 +8,7 @@ Information on how to install and use the software can be found in [the manual](
|
||||||
|
|
||||||
The main repository for The Arsse can be found at [code.mensbeam.com](https://code.mensbeam.com/MensBeam/arsse/), with a mirror also available [at GitHub](https://github.com/mensbeam/arsse/). The GitHub mirror does not accept bug reports, but the two should otherwise be equivalent.
|
The main repository for The Arsse can be found at [code.mensbeam.com](https://code.mensbeam.com/MensBeam/arsse/), with a mirror also available [at GitHub](https://github.com/mensbeam/arsse/). The GitHub mirror does not accept bug reports, but the two should otherwise be equivalent.
|
||||||
|
|
||||||
[Composer](https://getcomposer.org/) is required to manage PHP dependencies. After cloning the repository or downloading a source code tarball, running `composer install` will download all the required dependencies, and will advise if any PHP extensions need to be installed. If not installing as a programming environment, running `composer install --no-dev` is recommended.
|
[Composer](https://getcomposer.org/) is required to manage PHP dependencies. After cloning the repository or downloading a source code tarball, running `composer install` will download all the required dependencies, and will advise if any PHP extensions need to be installed. If not installing as a programming environment, running `composer install -o --no-dev --no-scripts` is recommended.
|
||||||
|
|
||||||
# Repository structure
|
# Repository structure
|
||||||
|
|
||||||
|
@ -34,13 +34,15 @@ Also necessary to the functioning of the application is the `/vendor/` directory
|
||||||
|
|
||||||
The `/locale/` and `/sql/` directories contain human-language files and database schemata, both of which are occasionally used by the application in the course of execution. The `/www/` directory serves as a document root for a few static files to be made available to users by a Web server.
|
The `/locale/` and `/sql/` directories contain human-language files and database schemata, both of which are occasionally used by the application in the course of execution. The `/www/` directory serves as a document root for a few static files to be made available to users by a Web server.
|
||||||
|
|
||||||
The `/dist/` directory, on the other hand, contains samples of configuration for Web servers and init systems. These are not used by The Arsse itself, but are merely distributed with it for reference.
|
The `/dist/` directory, on the other hand, contains general and system-specific build files, and samples of configuration for Web servers and other system integration. These are not used by The Arsse itself, but are used during the process of preparing new releases for supported operating systems.
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
The source text for The Arsse's manual can be found in `/docs/`, with pages written in [Markdown](https://spec.commonmark.org/current/) and converted to HTML [with Daux](#building-the-manual). If a static manual is generated its files will appear under `/manual/`.
|
The source text for The Arsse's manual can be found in `/docs/`, with pages written in [Markdown](https://spec.commonmark.org/current/) and converted to HTML [with Daux](#building-the-manual). If a static manual is generated its files will appear under `/manual/`.
|
||||||
|
|
||||||
In addition to the manual the files `/README.md` (this file), `/CHANGELOG`, `/UPGRADING`, `/LICENSE`, and `/AUTHORS` also document various things about the software, rather than the software itself.
|
The Arsse also has a UNIX manual page, also written in Markdown, which can be found under `/manpages/`. [Pandoc](https://pandoc.org/) is needed to convert it to the appropriate format, with the results stored under `/dist/man/`.
|
||||||
|
|
||||||
|
In addition to the manuals the files `/README.md` (this file), `/CHANGELOG`, `/UPGRADING`, `/LICENSE`, and `/AUTHORS` also document various things about the software, rather than the software itself.
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
|
@ -50,7 +52,7 @@ The `/tests/` directory contains everything related to automated testing. It is
|
||||||
|--------------------|------------------------------------------------------------------------------------|
|
|--------------------|------------------------------------------------------------------------------------|
|
||||||
| `cases/` | The test cases themselves, organized in roughly the same structure as the code |
|
| `cases/` | The test cases themselves, organized in roughly the same structure as the code |
|
||||||
| `coverage/` | (optional) Generated code coverage reports |
|
| `coverage/` | (optional) Generated code coverage reports |
|
||||||
| `docroot/` | Sample documents used in some tests, to be returned by the PHP's basic HTTP server |
|
| `docroot/` | Sample documents used in some tests, to be returned by PHP's basic HTTP server |
|
||||||
| `lib/` | Supporting classes which do not contain test cases |
|
| `lib/` | Supporting classes which do not contain test cases |
|
||||||
| `bootstrap.php` | Bootstrap script, equivalent to `/arsse.php`, but for tests |
|
| `bootstrap.php` | Bootstrap script, equivalent to `/arsse.php`, but for tests |
|
||||||
| `phpunit.dist.xml` | PHPUnit configuration file |
|
| `phpunit.dist.xml` | PHPUnit configuration file |
|
||||||
|
@ -74,7 +76,7 @@ The `/vendor-bin/` directory houses the files needed for the tools used in The A
|
||||||
| `/robo` | Simple wrapper for executing Robo on POSIX systems |
|
| `/robo` | Simple wrapper for executing Robo on POSIX systems |
|
||||||
| `/robo.bat` | Simple wrapper for executing Robo on Windows |
|
| `/robo.bat` | Simple wrapper for executing Robo on Windows |
|
||||||
|
|
||||||
In addition the files `/package.json`, `/yarn.lock`, and `/postcss.config.js` as well as the `/node_modules/` directory are used by [Yarn](https://yarnpkg.com/) and [PostCSS](https://postcss.org/) when modifying the stylesheet for The Arsse's manual.
|
In addition the files `/package.json` and `/postcss.config.js` as well as the `/node_modules/` directory are used by [Yarn](https://yarnpkg.com/) and [PostCSS](https://postcss.org/) when modifying the stylesheet for The Arsse's manual.
|
||||||
|
|
||||||
# Common tasks
|
# Common tasks
|
||||||
|
|
||||||
|
@ -105,15 +107,20 @@ The Arsse's user manual, made using [Daux](https://daux.io/), can be compiled by
|
||||||
|
|
||||||
The manual employs a custom theme derived from the standard Daux theme. If the standard Daux theme receives improvements, the custom theme can be rebuilt by running `./robo manual:theme`. This requires that [NodeJS](https://nodejs.org) and [Yarn](https://yarnpkg.com/) be installed, but JavaScript tools are not required to modify The Arsse itself, nor the content of the manual.
|
The manual employs a custom theme derived from the standard Daux theme. If the standard Daux theme receives improvements, the custom theme can be rebuilt by running `./robo manual:theme`. This requires that [NodeJS](https://nodejs.org) and [Yarn](https://yarnpkg.com/) be installed, but JavaScript tools are not required to modify The Arsse itself, nor the content of the manual.
|
||||||
|
|
||||||
|
## Building the man page
|
||||||
|
|
||||||
|
The Arsse's UNIX manual page is authored in Markdown, and must be converted to the native roff format using [Pandoc](https://pandoc.org/). This can be done by running `./robo manpage`, which will output appropriate files to `/dist/man/`. The conversion should not be done manually as there is post-processing required for optimal output.
|
||||||
|
|
||||||
## Packaging a release
|
## Packaging a release
|
||||||
|
|
||||||
Producing a release package is done by running `./robo package`. This performs the following operations:
|
Producing release packages is done by running `./robo package`. This performs the following operations:
|
||||||
|
|
||||||
- Duplicates a working tree with the commit (usually a release tag) to package
|
- Duplicates a [Git](https://git-scm.com/) working tree with the commit (usually a release tag) to package
|
||||||
- Generates the manual
|
- Generates UNIX manual pages with [Pandoc](https://pandoc.org/)
|
||||||
|
- Generates the HTML manual
|
||||||
- Installs runtime Composer dependencies with an optimized autoloader
|
- Installs runtime Composer dependencies with an optimized autoloader
|
||||||
- Deletes numerous unneeded files
|
- Deletes numerous unneeded files
|
||||||
- Exports the default configuration of The Arsse to a file
|
- Exports the default configuration of The Arsse to a file
|
||||||
- Compresses the remaining files into a tarball
|
- Compresses the remaining files into a tarball
|
||||||
|
- Produces a binary package for Arch Linux, if possible
|
||||||
Due to the first step, [Git](https://git-scm.com/) is required to package a release.
|
- Produces source and binary packages for Debian using [pbuilder](https://pbuilder-team.pages.debian.net/pbuilder/), if possible
|
||||||
|
|
411
RoboFile.php
411
RoboFile.php
|
@ -1,11 +1,14 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use Robo\Collection\CollectionBuilder;
|
||||||
use Robo\Result;
|
use Robo\Result;
|
||||||
|
|
||||||
const BASE = __DIR__.\DIRECTORY_SEPARATOR;
|
const BASE = __DIR__.\DIRECTORY_SEPARATOR;
|
||||||
const BASE_TEST = BASE."tests".\DIRECTORY_SEPARATOR;
|
const BASE_TEST = BASE."tests".\DIRECTORY_SEPARATOR;
|
||||||
define("IS_WIN", defined("PHP_WINDOWS_VERSION_MAJOR"));
|
define("IS_WIN", defined("PHP_WINDOWS_VERSION_MAJOR"));
|
||||||
define("IS_MAC", php_uname("s") === "Darwin");
|
define("IS_MAC", php_uname("s") === "Darwin");
|
||||||
|
define("IS_LINUX", !IS_WIN && !IS_MAC);
|
||||||
|
error_reporting(0);
|
||||||
|
|
||||||
function norm(string $path): string {
|
function norm(string $path): string {
|
||||||
$out = realpath($path);
|
$out = realpath($path);
|
||||||
|
@ -96,11 +99,11 @@ class RoboFile extends \Robo\Tasks {
|
||||||
if (extension_loaded("pcov")) {
|
if (extension_loaded("pcov")) {
|
||||||
return "$php -d pcov.enabled=1 -d pcov.directory=$code";
|
return "$php -d pcov.enabled=1 -d pcov.directory=$code";
|
||||||
} elseif (extension_loaded("xdebug")) {
|
} elseif (extension_loaded("xdebug")) {
|
||||||
return $php;
|
return "$php -d xdebug.mode=coverage";
|
||||||
} elseif (file_exists($dir."pcov.$ext")) {
|
} elseif (file_exists($dir."pcov.$ext")) {
|
||||||
return "$php -d extension=pcov.$ext -d pcov.enabled=1 -d pcov.directory=$code";
|
return "$php -d extension=pcov.$ext -d pcov.enabled=1 -d pcov.directory=$code";
|
||||||
} elseif (file_exists($dir."pcov.$ext")) {
|
} elseif (file_exists($dir."xdebug.$ext")) {
|
||||||
return "$php -d zend_extension=xdebug.$ext";
|
return "$php -d zend_extension=xdebug.$ext -d xdebug.mode=coverage";
|
||||||
} else {
|
} else {
|
||||||
if (IS_WIN) {
|
if (IS_WIN) {
|
||||||
$dbg = dirname(\PHP_BINARY)."\\phpdbg.exe";
|
$dbg = dirname(\PHP_BINARY)."\\phpdbg.exe";
|
||||||
|
@ -144,62 +147,239 @@ 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();
|
return $this->taskExec($executor)->option("-d", "zend.assertions=1")->arg($execpath)->option("-c", $confpath)->args(array_merge($set, $args))->run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function commitVersion(?string $commit): array {
|
||||||
|
$target = $commit ?? $this->askDefault("Reference commit:", "HEAD");
|
||||||
|
$base = escapeshellarg(BASE);
|
||||||
|
$blackhole = $this->blackhole();
|
||||||
|
// get useable version strings from Git
|
||||||
|
$version = trim(`git -C $base describe --tags $target $blackhole`);
|
||||||
|
if (!$version) {
|
||||||
|
throw new \Exception("Commit reference invalid");
|
||||||
|
}
|
||||||
|
return [$target, $version];
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/** Packages a given commit of the software into a release tarball
|
/** Packages a given commit of the software into a release tarball
|
||||||
*
|
*
|
||||||
* The version to package may be any Git tree-ish identifier: a tag, a branch,
|
* 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
|
* or any commit hash. If none is provided on the command line, Robo will prompt
|
||||||
* for a commit to package; the default is "head".
|
* for a commit to package; the default is "HEAD".
|
||||||
*
|
*
|
||||||
* Note that while it is possible to re-package old versions, the resultant tarball
|
* Note that while it is possible to re-package old versions, the resultant tarball
|
||||||
* may not be equivalent due to subsequent changes in the exclude list, or because
|
* may not be equivalent due to subsequent changes in the exclude list, or because
|
||||||
* of new tooling.
|
* of new tooling.
|
||||||
*/
|
*/
|
||||||
public function package(string $version = null): Result {
|
public function packageGeneric(string $commit = null): Result {
|
||||||
|
if (!$this->toolExists("git", "pandoc")) {
|
||||||
|
throw new \Exception("Git and Pandoc are required in PATH to produce generic release tarballs");
|
||||||
|
}
|
||||||
// establish which commit to package
|
// establish which commit to package
|
||||||
$version = $version ?? $this->askDefault("Commit to package:", "HEAD");
|
[$commit, $version] = $this->commitVersion($commit);
|
||||||
$archive = BASE."arsse-$version.tar.gz";
|
$archVersion = preg_replace('/^([^-]+)-(\d+)-(\w+)$/', "$1.r$2.$3", $version);
|
||||||
|
// name the generic release tarball
|
||||||
|
$tarball = BASE."release/$version/arsse-$version.tar.gz";
|
||||||
// start a collection
|
// start a collection
|
||||||
$t = $this->collectionBuilder();
|
$t = $this->collectionBuilder();
|
||||||
// create a temporary directory
|
// create a temporary directory
|
||||||
$dir = $t->tmpDir().\DIRECTORY_SEPARATOR;
|
$dir = $t->tmpDir().\DIRECTORY_SEPARATOR;
|
||||||
// create a Git worktree for the selected commit in the temp location
|
// create a Git worktree for the selected commit in the temp location
|
||||||
$t->taskExec("git worktree add ".escapeshellarg($dir)." ".escapeshellarg($version));
|
$result = $this->taskExec("git worktree add ".escapeshellarg($dir)." ".escapeshellarg($version))->dir(BASE)->run();
|
||||||
// perform Composer installation in the temp location with dev dependencies
|
if ($result->getExitCode() > 0) {
|
||||||
$t->taskComposerInstall()->dir($dir);
|
return $result;
|
||||||
// generate the manual
|
}
|
||||||
$t->taskExec(escapeshellarg($dir."robo")." manual")->dir($dir);
|
try {
|
||||||
// perform Composer installation in the temp location for final output
|
// generate the Debian changelog; this also validates our original changelog
|
||||||
$t->taskComposerInstall()->dir($dir)->noDev()->optimizeAutoloader()->arg("--no-scripts");
|
$debianChangelog = $this->changelogDebian($this->changelogParse(file_get_contents($dir."CHANGELOG"), $version), $version);
|
||||||
// delete unwanted files
|
// save commit description to VERSION file for reference
|
||||||
$t->taskFilesystemStack()->remove([
|
$t->addTask($this->taskWriteToFile($dir."VERSION")->text($version));
|
||||||
$dir.".git",
|
// patch the Arch PKGBUILD file with the correct version string
|
||||||
$dir.".gitignore",
|
$t->addTask($this->taskReplaceInFile($dir."dist/arch/PKGBUILD")->regex('/^pkgver=.*$/m')->to("pkgver=$archVersion"));
|
||||||
$dir.".gitattributes",
|
// patch the Arch PKGBUILD file with the correct source file
|
||||||
$dir."composer.json",
|
$t->addTask($this->taskReplaceInFile($dir."dist/arch/PKGBUILD")->regex('/^source=\("arsse-[^"]+"\)$/m')->to('source=("'.basename($tarball).'")'));
|
||||||
$dir."composer.lock",
|
// save the Debian-format changelog
|
||||||
$dir.".php_cs.dist",
|
$t->addTask($this->taskWriteToFile($dir."dist/debian/changelog")->text($debianChangelog));
|
||||||
$dir."phpdoc.dist.xml",
|
// perform Composer installation in the temp location with dev dependencies
|
||||||
$dir."build.xml",
|
$t->addTask($this->taskComposerInstall()->arg("-q")->dir($dir));
|
||||||
$dir."RoboFile.php",
|
// generate manpages
|
||||||
$dir."CONTRIBUTING.md",
|
$t->addTask($this->taskExec("./robo manpage")->dir($dir));
|
||||||
$dir."docs",
|
// generate the HTML manual
|
||||||
$dir."tests",
|
$t->addTask($this->taskExec("./robo manual -q")->dir($dir));
|
||||||
$dir."vendor-bin",
|
// perform Composer installation in the temp location for final output
|
||||||
$dir."vendor/bin",
|
$t->addTask($this->taskComposerInstall()->dir($dir)->noDev()->optimizeAutoloader()->arg("--no-scripts")->arg("-q"));
|
||||||
$dir."robo",
|
// delete unwanted files
|
||||||
$dir."robo.bat",
|
$t->addTask($this->taskFilesystemStack()->remove([
|
||||||
$dir."package.json",
|
$dir.".git",
|
||||||
$dir."yarn.lock",
|
$dir.".gitignore",
|
||||||
$dir."postcss.config.js",
|
$dir.".gitattributes",
|
||||||
]);
|
$dir."dist/debian/.gitignore",
|
||||||
// generate a sample configuration file
|
$dir."composer.json",
|
||||||
$t->taskExec(escapeshellarg(\PHP_BINARY)." arsse.php conf save-defaults config.defaults.php")->dir($dir);
|
$dir."composer.lock",
|
||||||
// package it all up
|
$dir.".php_cs.dist",
|
||||||
$t->taskPack($archive)->addDir("arsse", $dir);
|
$dir."phpdoc.dist.xml",
|
||||||
// execute the collection
|
$dir."build.xml",
|
||||||
|
$dir."RoboFile.php",
|
||||||
|
$dir."CONTRIBUTING.md",
|
||||||
|
$dir."docs",
|
||||||
|
$dir."manpages",
|
||||||
|
$dir."tests",
|
||||||
|
$dir."vendor-bin",
|
||||||
|
$dir."vendor/bin",
|
||||||
|
$dir."robo",
|
||||||
|
$dir."robo.bat",
|
||||||
|
$dir."package.json",
|
||||||
|
$dir."yarn.lock",
|
||||||
|
$dir."postcss.config.js",
|
||||||
|
]));
|
||||||
|
$t->addCode(function() use ($dir) {
|
||||||
|
// Remove files which lintian complains about; they're otherwise harmless
|
||||||
|
$files = [];
|
||||||
|
foreach (new \CallbackFilterIterator(new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir."vendor", \FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS)), function($v, $k, $i) {
|
||||||
|
return preg_match('/\/\.git(?:ignore|attributes|modules)$/D', $v);
|
||||||
|
}) as $f) {
|
||||||
|
$files[] = $f;
|
||||||
|
}
|
||||||
|
return $this->taskFilesystemStack()->remove($files)->run();
|
||||||
|
});
|
||||||
|
// generate a sample configuration file
|
||||||
|
$t->addTask($this->taskExec(escapeshellarg(\PHP_BINARY)." arsse.php conf save-defaults config.defaults.php")->dir($dir));
|
||||||
|
// remove any existing archive
|
||||||
|
$t->addTask($this->taskFilesystemStack()->remove($tarball));
|
||||||
|
// package it all up
|
||||||
|
$t->addTask($this->taskFilesystemStack()->mkdir(dirname($tarball)));
|
||||||
|
$t->addTask($this->taskPack($tarball)->addDir("arsse", $dir));
|
||||||
|
// execute the collection
|
||||||
|
$result = $t->run();
|
||||||
|
} finally {
|
||||||
|
// remove the Git worktree
|
||||||
|
$this->taskFilesystemStack()->remove($dir)->run();
|
||||||
|
$this->taskExec("git worktree prune")->dir(BASE)->run();
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Packages a given commit of the software into an Arch 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");
|
||||||
|
}
|
||||||
|
// 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
|
||||||
|
$t = $this->collectionBuilder();
|
||||||
|
// 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));
|
||||||
|
return $t->run();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Packages a given commit of the software into source and binary Debian packages
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
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"),
|
||||||
|
];
|
||||||
|
// 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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
$out = $t->run();
|
$out = $t->run();
|
||||||
// clean the Git worktree list
|
// note any packages which were not built
|
||||||
$this->_exec("git worktree prune");
|
foreach ($dist as $distro => $run) {
|
||||||
|
if (!$run) {
|
||||||
|
$this->say("Packages for $distro skipped");
|
||||||
|
}
|
||||||
|
}
|
||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,6 +409,9 @@ class RoboFile extends \Robo\Tasks {
|
||||||
* Daux's theme changes
|
* Daux's theme changes
|
||||||
*/
|
*/
|
||||||
public function manualTheme(array $args): Result {
|
public function manualTheme(array $args): Result {
|
||||||
|
if (!$this->toolExists("yarn")) {
|
||||||
|
throw new \Exception("Yarn is required in PATH to update the Daux theme");
|
||||||
|
}
|
||||||
$postcss = escapeshellarg(norm(BASE."node_modules/.bin/postcss"));
|
$postcss = escapeshellarg(norm(BASE."node_modules/.bin/postcss"));
|
||||||
$themesrc = norm(BASE."docs/theme/src/").\DIRECTORY_SEPARATOR;
|
$themesrc = norm(BASE."docs/theme/src/").\DIRECTORY_SEPARATOR;
|
||||||
$themeout = norm(BASE."docs/theme/arsse/").\DIRECTORY_SEPARATOR;
|
$themeout = norm(BASE."docs/theme/arsse/").\DIRECTORY_SEPARATOR;
|
||||||
|
@ -246,4 +429,144 @@ class RoboFile extends \Robo\Tasks {
|
||||||
// execute the collection
|
// execute the collection
|
||||||
return $t->run();
|
return $t->run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Generates the "arsse" command's manual page (UNIX man page)
|
||||||
|
*
|
||||||
|
* This requires that the Pandoc document converter be installed and
|
||||||
|
* available in $PATH.
|
||||||
|
*/
|
||||||
|
public function manpage(): Result {
|
||||||
|
if (!$this->toolExists("pandoc")) {
|
||||||
|
throw new \Exception("Pandoc is required in PATH to generate manual pages");
|
||||||
|
}
|
||||||
|
$t = $this->collectionBuilder();
|
||||||
|
$man = [
|
||||||
|
'en' => "man1/arsse.1",
|
||||||
|
];
|
||||||
|
foreach ($man as $src => $out) {
|
||||||
|
$src = BASE."manpages/$src.md";
|
||||||
|
$out = BASE."dist/man/$out";
|
||||||
|
$t->addTask($this->taskFilesystemStack()->mkdir(dirname($out), 0755));
|
||||||
|
$t->addTask($this->taskExec("pandoc -s -f markdown-smart -t man -o ".escapeshellarg($out)." ".escapeshellarg($src)));
|
||||||
|
$t->addTask($this->taskReplaceInFile($out)->regex('/\.\n(?!\.)/s')->to(". "));
|
||||||
|
}
|
||||||
|
return $t->run();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function changelogParse(string $text, string $targetVersion): array {
|
||||||
|
$lines = preg_split('/\r?\n/', $text);
|
||||||
|
$version = "";
|
||||||
|
$section = "";
|
||||||
|
$out = [];
|
||||||
|
$entry = [];
|
||||||
|
$expected = ["version"];
|
||||||
|
for ($a = 0; $a < sizeof($lines);) {
|
||||||
|
$l = rtrim($lines[$a++]);
|
||||||
|
if (in_array("version", $expected) && preg_match('/^Version (\d+(?:\.\d+)*) \(([\d\?]{4}-[\d\?]{2}-[\d\?]{2})\)\s*$/D', $l, $m)) {
|
||||||
|
$version = $m[1];
|
||||||
|
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/D', $m[2])) {
|
||||||
|
// uncertain dates are allowed only for the top version, and only if it does not match the target version (otherwise we have forgotten to set the correct date before tagging)
|
||||||
|
if (!$out && $targetVersion !== $version) {
|
||||||
|
// use today's date; local time is fine
|
||||||
|
$date = date("Y-m-d");
|
||||||
|
} else {
|
||||||
|
throw new \Exception("CHANGELOG: Date at line $a is incomplete");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$date = $m[2];
|
||||||
|
}
|
||||||
|
if ($entry) {
|
||||||
|
$out[] = $entry;
|
||||||
|
}
|
||||||
|
$entry = ['version' => $version, 'date' => $date, 'features' => [], 'fixes' => [], 'changes' => []];
|
||||||
|
$expected = ["separator"];
|
||||||
|
} elseif (in_array("separator", $expected) && preg_match('/^=+/', $l)) {
|
||||||
|
$length = strlen($lines[$a - 2]);
|
||||||
|
if (strlen($l) !== $length) {
|
||||||
|
throw new \Exception("CHANGELOG: Separator at line $a is of incorrect length");
|
||||||
|
}
|
||||||
|
$expected = ["blank line"];
|
||||||
|
$section = "";
|
||||||
|
} elseif (in_array("blank line", $expected) && $l === "") {
|
||||||
|
$expected = [
|
||||||
|
'' => ["features section", "fixes section", "changes section"],
|
||||||
|
'features' => ["fixes section", "changes section", "version"],
|
||||||
|
'fixes' => ["changes section", "version"],
|
||||||
|
'changes' => ["version"],
|
||||||
|
][$section];
|
||||||
|
$expected[] = "end-of-file";
|
||||||
|
} elseif (in_array("features section", $expected) && $l === "New features:") {
|
||||||
|
$section = "features";
|
||||||
|
$expected = ["item"];
|
||||||
|
} elseif (in_array("fixes section", $expected) && $l === "Bug fixes:") {
|
||||||
|
$section = "fixes";
|
||||||
|
$expected = ["item"];
|
||||||
|
} elseif (in_array("changes section", $expected) && $l === "Changes:") {
|
||||||
|
$section = "changes";
|
||||||
|
$expected = ["item"];
|
||||||
|
} elseif (in_array("item", $expected) && preg_match('/^- (\w.*)$/D', $l, $m)) {
|
||||||
|
$entry[$section][] = $m[1];
|
||||||
|
$expected = ["item", "continuation", "blank line"];
|
||||||
|
} elseif (in_array("continuation", $expected) && preg_match('/^ (\w.*)$/D', $l, $m)) {
|
||||||
|
$last = sizeof($entry[$section]) - 1;
|
||||||
|
$entry[$section][$last] .= "\n".$m[1];
|
||||||
|
} else {
|
||||||
|
if (sizeof($expected) > 1) {
|
||||||
|
throw new \Exception("CHANGELOG: Expected one of [".implode(", ", $expected)."] at line $a");
|
||||||
|
} else {
|
||||||
|
throw new \Exception("CHANGELOG: Expected ".$expected[0]." at line $a");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!in_array("end-of-file", $expected)) {
|
||||||
|
if (sizeof($expected) > 1) {
|
||||||
|
throw new \Exception("CHANGELOG: Expected one of [".implode(", ", $expected)."] at end of file");
|
||||||
|
} else {
|
||||||
|
throw new \Exception("CHANGELOG: Expected ".$expected[0]." at end of file");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$out[] = $entry;
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function changelogDebian(array $log, string $targetVersion): string {
|
||||||
|
$latest = $log[0]['version'];
|
||||||
|
$baseVersion = preg_replace('/^(\d+(?:\.\d+)*).*/', "$1", $targetVersion);
|
||||||
|
if ($baseVersion !== $targetVersion && version_compare($latest, $baseVersion, ">")) {
|
||||||
|
// if the changelog contains an entry for a future version, change its version number to match the target version instead of using the future version
|
||||||
|
$log[0]['version'] = $targetVersion;
|
||||||
|
$log[0]['distribution'] = "UNRELEASED";
|
||||||
|
} elseif ($baseVersion !== $targetVersion) {
|
||||||
|
// otherwise synthesize a changelog entry for the changes since the last tag
|
||||||
|
array_unshift($log, ['version' => $targetVersion, 'date' => date("Y-m-d"), 'features' => [], 'fixes' => [], 'changes' => ["Unspecified changes"], 'distribution' => "UNRELEASED"]);
|
||||||
|
}
|
||||||
|
$out = "";
|
||||||
|
foreach ($log as $entry) {
|
||||||
|
// normalize the version string
|
||||||
|
preg_match('/^(\d+(?:\.\d+)*)(?:-(\d+)-.+)?$/D', $entry['version'], $m);
|
||||||
|
$version = $m[1]."-".($m[2] ?: "1");
|
||||||
|
// output the entry
|
||||||
|
$out .= "arsse ($version) ".($entry['distribution'] ?? "unstable")."; urgency=low\n";
|
||||||
|
if ($entry['features']) {
|
||||||
|
$out .= "\n";
|
||||||
|
foreach ($entry['features'] as $item) {
|
||||||
|
$out .= " * ".trim(preg_replace("/^/m", " ", $item))."\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($entry['fixes']) {
|
||||||
|
$out .= "\n";
|
||||||
|
foreach ($entry['fixes'] as $item) {
|
||||||
|
$out .= " * ".trim(preg_replace("/^/m", " ", $item))."\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($entry['changes']) {
|
||||||
|
$out .= "\n";
|
||||||
|
foreach ($entry['changes'] as $item) {
|
||||||
|
$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";
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
17
UPGRADING
17
UPGRADING
|
@ -11,6 +11,23 @@ usually prudent:
|
||||||
`composer install -o --no-dev`
|
`composer install -o --no-dev`
|
||||||
|
|
||||||
|
|
||||||
|
Upgrading from 0.8.5 to 0.9.0
|
||||||
|
=============================
|
||||||
|
|
||||||
|
- The database schema has changed from rev6 to rev7; if upgrading the database
|
||||||
|
manually, apply the 6.sql file
|
||||||
|
- Web server configuration has changed to accommodate Miniflux; the following
|
||||||
|
URL paths are affected:
|
||||||
|
- /v1/
|
||||||
|
- /version
|
||||||
|
- /healthcheck
|
||||||
|
- Icons for existing feeds in Miniflux and Fever will only appear once the
|
||||||
|
feeds in question have been fetched and parsed after upgrade. This may take
|
||||||
|
some time to occur depending on how often the feed is updated
|
||||||
|
- An administrator account is now required to refresh feeds via the
|
||||||
|
Nextcloud News protocol
|
||||||
|
|
||||||
|
|
||||||
Upgrading from 0.8.4 to 0.8.5
|
Upgrading from 0.8.4 to 0.8.5
|
||||||
=============================
|
=============================
|
||||||
|
|
||||||
|
|
|
@ -13,17 +13,19 @@ require_once BASE."vendor".DIRECTORY_SEPARATOR."autoload.php";
|
||||||
ignore_user_abort(true);
|
ignore_user_abort(true);
|
||||||
ini_set("memory_limit", "-1");
|
ini_set("memory_limit", "-1");
|
||||||
ini_set("max_execution_time", "0");
|
ini_set("max_execution_time", "0");
|
||||||
|
// FIXME: This is required by a dependency of Picofeed
|
||||||
|
error_reporting(\E_ALL & ~\E_DEPRECATED);
|
||||||
|
|
||||||
if (\PHP_SAPI === "cli") {
|
if (\PHP_SAPI === "cli") {
|
||||||
// initialize the CLI; this automatically handles --help and --version
|
// initialize the CLI; this automatically handles --help and --version else
|
||||||
|
Arsse::$obj = new Factory;
|
||||||
$cli = new CLI;
|
$cli = new CLI;
|
||||||
// handle other CLI requests; some do not require configuration
|
// handle other CLI requests; some do not require configuration
|
||||||
$exitStatus = $cli->dispatch();
|
$exitStatus = $cli->dispatch();
|
||||||
exit($exitStatus);
|
exit($exitStatus);
|
||||||
} else {
|
} else {
|
||||||
// load configuration
|
// load configuration
|
||||||
$conf = file_exists(BASE."config.php") ? new Conf(BASE."config.php") : new Conf;
|
Arsse::bootstrap();
|
||||||
Arsse::load($conf);
|
|
||||||
// handle Web requests
|
// handle Web requests
|
||||||
$emitter = new \Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
|
$emitter = new \Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
|
||||||
$response = (new REST)->dispatch();
|
$response = (new REST)->dispatch();
|
||||||
|
|
|
@ -18,10 +18,11 @@
|
||||||
|
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^7.1",
|
"php": "^7.1 || ^8.0",
|
||||||
"ext-intl": "*",
|
"ext-intl": "*",
|
||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
"ext-hash": "*",
|
"ext-hash": "*",
|
||||||
|
"ext-filter": "*",
|
||||||
"ext-dom": "*",
|
"ext-dom": "*",
|
||||||
"nicolus/picofeed": "^0.1.43",
|
"nicolus/picofeed": "^0.1.43",
|
||||||
"hosteurope/password-generator": "1.*",
|
"hosteurope/password-generator": "1.*",
|
||||||
|
@ -33,6 +34,9 @@
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"bamarni/composer-bin-plugin": "*"
|
"bamarni/composer-bin-plugin": "*"
|
||||||
},
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-pcntl": "To respond to signals, particular to reload configuration via SIGHUP"
|
||||||
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"platform": {
|
"platform": {
|
||||||
"php": "7.1.33"
|
"php": "7.1.33"
|
||||||
|
|
166
composer.lock
generated
166
composer.lock
generated
|
@ -4,7 +4,7 @@
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "9880398f241b2e782876bb510207cde7",
|
"content-hash": "c658930fbc56b2b2cf646e34c6a8d8d3",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "docopt/docopt",
|
"name": "docopt/docopt",
|
||||||
|
@ -50,6 +50,10 @@
|
||||||
"cli",
|
"cli",
|
||||||
"docs"
|
"docs"
|
||||||
],
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/docopt/docopt.php/issues",
|
||||||
|
"source": "https://github.com/docopt/docopt.php/tree/1.0.4"
|
||||||
|
},
|
||||||
"time": "2019-12-03T02:48:46+00:00"
|
"time": "2019-12-03T02:48:46+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -117,20 +121,24 @@
|
||||||
"rest",
|
"rest",
|
||||||
"web service"
|
"web service"
|
||||||
],
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/guzzle/guzzle/issues",
|
||||||
|
"source": "https://github.com/guzzle/guzzle/tree/6.5"
|
||||||
|
},
|
||||||
"time": "2020-06-16T21:01:06+00:00"
|
"time": "2020-06-16T21:01:06+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "guzzlehttp/promises",
|
"name": "guzzlehttp/promises",
|
||||||
"version": "1.4.0",
|
"version": "1.4.1",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/guzzle/promises.git",
|
"url": "https://github.com/guzzle/promises.git",
|
||||||
"reference": "60d379c243457e073cff02bc323a2a86cb355631"
|
"reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/guzzle/promises/zipball/60d379c243457e073cff02bc323a2a86cb355631",
|
"url": "https://api.github.com/repos/guzzle/promises/zipball/8e7d04f1f6450fef59366c399cfad4b9383aa30d",
|
||||||
"reference": "60d379c243457e073cff02bc323a2a86cb355631",
|
"reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
@ -168,20 +176,24 @@
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"promise"
|
"promise"
|
||||||
],
|
],
|
||||||
"time": "2020-09-30T07:37:28+00:00"
|
"support": {
|
||||||
|
"issues": "https://github.com/guzzle/promises/issues",
|
||||||
|
"source": "https://github.com/guzzle/promises/tree/1.4.1"
|
||||||
|
},
|
||||||
|
"time": "2021-03-07T09:25:29+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "guzzlehttp/psr7",
|
"name": "guzzlehttp/psr7",
|
||||||
"version": "1.7.0",
|
"version": "1.8.2",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/guzzle/psr7.git",
|
"url": "https://github.com/guzzle/psr7.git",
|
||||||
"reference": "53330f47520498c0ae1f61f7e2c90f55690c06a3"
|
"reference": "dc960a912984efb74d0a90222870c72c87f10c91"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/guzzle/psr7/zipball/53330f47520498c0ae1f61f7e2c90f55690c06a3",
|
"url": "https://api.github.com/repos/guzzle/psr7/zipball/dc960a912984efb74d0a90222870c72c87f10c91",
|
||||||
"reference": "53330f47520498c0ae1f61f7e2c90f55690c06a3",
|
"reference": "dc960a912984efb74d0a90222870c72c87f10c91",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
@ -239,7 +251,11 @@
|
||||||
"uri",
|
"uri",
|
||||||
"url"
|
"url"
|
||||||
],
|
],
|
||||||
"time": "2020-09-30T07:37:11+00:00"
|
"support": {
|
||||||
|
"issues": "https://github.com/guzzle/psr7/issues",
|
||||||
|
"source": "https://github.com/guzzle/psr7/tree/1.8.2"
|
||||||
|
},
|
||||||
|
"time": "2021-04-26T09:17:50+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "hosteurope/password-generator",
|
"name": "hosteurope/password-generator",
|
||||||
|
@ -279,6 +295,10 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Password generator for generating policy-compliant passwords.",
|
"description": "Password generator for generating policy-compliant passwords.",
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/hosteurope/password-generator/issues",
|
||||||
|
"source": "https://github.com/hosteurope/password-generator/tree/master"
|
||||||
|
},
|
||||||
"time": "2016-12-08T09:32:12+00:00"
|
"time": "2016-12-08T09:32:12+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -324,6 +344,10 @@
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"uuid"
|
"uuid"
|
||||||
],
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/JKingweb/DrUUID/issues",
|
||||||
|
"source": "https://github.com/JKingweb/DrUUID/tree/3.0.0"
|
||||||
|
},
|
||||||
"time": "2017-02-09T14:17:01+00:00"
|
"time": "2017-02-09T14:17:01+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -399,6 +423,10 @@
|
||||||
"rfc7234",
|
"rfc7234",
|
||||||
"validation"
|
"validation"
|
||||||
],
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/Kevinrob/guzzle-cache-middleware/issues",
|
||||||
|
"source": "https://github.com/Kevinrob/guzzle-cache-middleware/tree/master"
|
||||||
|
},
|
||||||
"time": "2017-08-17T12:23:43+00:00"
|
"time": "2017-08-17T12:23:43+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -484,6 +512,14 @@
|
||||||
"psr-17",
|
"psr-17",
|
||||||
"psr-7"
|
"psr-7"
|
||||||
],
|
],
|
||||||
|
"support": {
|
||||||
|
"chat": "https://laminas.dev/chat",
|
||||||
|
"docs": "https://docs.laminas.dev/laminas-diactoros/",
|
||||||
|
"forum": "https://discourse.laminas.dev",
|
||||||
|
"issues": "https://github.com/laminas/laminas-diactoros/issues",
|
||||||
|
"rss": "https://github.com/laminas/laminas-diactoros/releases.atom",
|
||||||
|
"source": "https://github.com/laminas/laminas-diactoros"
|
||||||
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"url": "https://funding.communitybridge.org/projects/laminas-project",
|
"url": "https://funding.communitybridge.org/projects/laminas-project",
|
||||||
|
@ -549,6 +585,14 @@
|
||||||
"psr-15",
|
"psr-15",
|
||||||
"psr-7"
|
"psr-7"
|
||||||
],
|
],
|
||||||
|
"support": {
|
||||||
|
"chat": "https://laminas.dev/chat",
|
||||||
|
"docs": "https://docs.laminas.dev/laminas-httphandlerrunner/",
|
||||||
|
"forum": "https://discourse.laminas.dev",
|
||||||
|
"issues": "https://github.com/laminas/laminas-httphandlerrunner/issues",
|
||||||
|
"rss": "https://github.com/laminas/laminas-httphandlerrunner/releases.atom",
|
||||||
|
"source": "https://github.com/laminas/laminas-httphandlerrunner"
|
||||||
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"url": "https://funding.communitybridge.org/projects/laminas-project",
|
"url": "https://funding.communitybridge.org/projects/laminas-project",
|
||||||
|
@ -605,6 +649,14 @@
|
||||||
"security",
|
"security",
|
||||||
"xml"
|
"xml"
|
||||||
],
|
],
|
||||||
|
"support": {
|
||||||
|
"chat": "https://laminas.dev/chat",
|
||||||
|
"docs": "https://docs.laminas.dev/laminas-xml/",
|
||||||
|
"forum": "https://discourse.laminas.dev",
|
||||||
|
"issues": "https://github.com/laminas/laminas-xml/issues",
|
||||||
|
"rss": "https://github.com/laminas/laminas-xml/releases.atom",
|
||||||
|
"source": "https://github.com/laminas/laminas-xml"
|
||||||
|
},
|
||||||
"time": "2019-12-31T18:05:42+00:00"
|
"time": "2019-12-31T18:05:42+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -653,6 +705,12 @@
|
||||||
"laminas",
|
"laminas",
|
||||||
"zf"
|
"zf"
|
||||||
],
|
],
|
||||||
|
"support": {
|
||||||
|
"forum": "https://discourse.laminas.dev/",
|
||||||
|
"issues": "https://github.com/laminas/laminas-zendframework-bridge/issues",
|
||||||
|
"rss": "https://github.com/laminas/laminas-zendframework-bridge/releases.atom",
|
||||||
|
"source": "https://github.com/laminas/laminas-zendframework-bridge"
|
||||||
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"url": "https://funding.communitybridge.org/projects/laminas-project",
|
"url": "https://funding.communitybridge.org/projects/laminas-project",
|
||||||
|
@ -721,6 +779,9 @@
|
||||||
],
|
],
|
||||||
"description": "RSS/Atom parsing library",
|
"description": "RSS/Atom parsing library",
|
||||||
"homepage": "https://github.com/nicolus/picoFeed",
|
"homepage": "https://github.com/nicolus/picoFeed",
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/nicolus/picoFeed/tree/0.1.43"
|
||||||
|
},
|
||||||
"time": "2020-09-15T07:28:23+00:00"
|
"time": "2020-09-15T07:28:23+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -773,6 +834,9 @@
|
||||||
"request",
|
"request",
|
||||||
"response"
|
"response"
|
||||||
],
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/php-fig/http-factory/tree/master"
|
||||||
|
},
|
||||||
"time": "2019-04-30T12:38:16+00:00"
|
"time": "2019-04-30T12:38:16+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -823,6 +887,9 @@
|
||||||
"request",
|
"request",
|
||||||
"response"
|
"response"
|
||||||
],
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/php-fig/http-message/tree/master"
|
||||||
|
},
|
||||||
"time": "2016-08-06T14:39:51+00:00"
|
"time": "2016-08-06T14:39:51+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -876,20 +943,24 @@
|
||||||
"response",
|
"response",
|
||||||
"server"
|
"server"
|
||||||
],
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/php-fig/http-server-handler/issues",
|
||||||
|
"source": "https://github.com/php-fig/http-server-handler/tree/master"
|
||||||
|
},
|
||||||
"time": "2018-10-30T16:46:14+00:00"
|
"time": "2018-10-30T16:46:14+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "psr/log",
|
"name": "psr/log",
|
||||||
"version": "1.1.3",
|
"version": "1.1.4",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/php-fig/log.git",
|
"url": "https://github.com/php-fig/log.git",
|
||||||
"reference": "0f73288fd15629204f9d42b7055f72dacbe811fc"
|
"reference": "d49695b909c3b7628b6289db5479a1c204601f11"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc",
|
"url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11",
|
||||||
"reference": "0f73288fd15629204f9d42b7055f72dacbe811fc",
|
"reference": "d49695b909c3b7628b6289db5479a1c204601f11",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
@ -913,7 +984,7 @@
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
"name": "PHP-FIG",
|
"name": "PHP-FIG",
|
||||||
"homepage": "http://www.php-fig.org/"
|
"homepage": "https://www.php-fig.org/"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Common interface for logging libraries",
|
"description": "Common interface for logging libraries",
|
||||||
|
@ -923,7 +994,10 @@
|
||||||
"psr",
|
"psr",
|
||||||
"psr-3"
|
"psr-3"
|
||||||
],
|
],
|
||||||
"time": "2020-03-23T09:12:05+00:00"
|
"support": {
|
||||||
|
"source": "https://github.com/php-fig/log/tree/1.1.4"
|
||||||
|
},
|
||||||
|
"time": "2021-05-03T11:20:27+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "ralouphie/getallheaders",
|
"name": "ralouphie/getallheaders",
|
||||||
|
@ -963,20 +1037,24 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "A polyfill for getallheaders.",
|
"description": "A polyfill for getallheaders.",
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/ralouphie/getallheaders/issues",
|
||||||
|
"source": "https://github.com/ralouphie/getallheaders/tree/develop"
|
||||||
|
},
|
||||||
"time": "2019-03-08T08:55:37+00:00"
|
"time": "2019-03-08T08:55:37+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-intl-idn",
|
"name": "symfony/polyfill-intl-idn",
|
||||||
"version": "v1.20.0",
|
"version": "v1.23.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/polyfill-intl-idn.git",
|
"url": "https://github.com/symfony/polyfill-intl-idn.git",
|
||||||
"reference": "3b75acd829741c768bc8b1f84eb33265e7cc5117"
|
"reference": "65bd267525e82759e7d8c4e8ceea44f398838e65"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/3b75acd829741c768bc8b1f84eb33265e7cc5117",
|
"url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/65bd267525e82759e7d8c4e8ceea44f398838e65",
|
||||||
"reference": "3b75acd829741c768bc8b1f84eb33265e7cc5117",
|
"reference": "65bd267525e82759e7d8c4e8ceea44f398838e65",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
@ -990,7 +1068,7 @@
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"extra": {
|
"extra": {
|
||||||
"branch-alias": {
|
"branch-alias": {
|
||||||
"dev-main": "1.20-dev"
|
"dev-main": "1.23-dev"
|
||||||
},
|
},
|
||||||
"thanks": {
|
"thanks": {
|
||||||
"name": "symfony/polyfill",
|
"name": "symfony/polyfill",
|
||||||
|
@ -1033,6 +1111,9 @@
|
||||||
"portable",
|
"portable",
|
||||||
"shim"
|
"shim"
|
||||||
],
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.23.0"
|
||||||
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"url": "https://symfony.com/sponsor",
|
"url": "https://symfony.com/sponsor",
|
||||||
|
@ -1047,20 +1128,20 @@
|
||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2020-10-23T14:02:19+00:00"
|
"time": "2021-05-27T09:27:20+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-intl-normalizer",
|
"name": "symfony/polyfill-intl-normalizer",
|
||||||
"version": "v1.20.0",
|
"version": "v1.23.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
|
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
|
||||||
"reference": "727d1096295d807c309fb01a851577302394c897"
|
"reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/727d1096295d807c309fb01a851577302394c897",
|
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8590a5f561694770bdcd3f9b5c69dde6945028e8",
|
||||||
"reference": "727d1096295d807c309fb01a851577302394c897",
|
"reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
@ -1072,7 +1153,7 @@
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"extra": {
|
"extra": {
|
||||||
"branch-alias": {
|
"branch-alias": {
|
||||||
"dev-main": "1.20-dev"
|
"dev-main": "1.23-dev"
|
||||||
},
|
},
|
||||||
"thanks": {
|
"thanks": {
|
||||||
"name": "symfony/polyfill",
|
"name": "symfony/polyfill",
|
||||||
|
@ -1114,6 +1195,9 @@
|
||||||
"portable",
|
"portable",
|
||||||
"shim"
|
"shim"
|
||||||
],
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.23.0"
|
||||||
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"url": "https://symfony.com/sponsor",
|
"url": "https://symfony.com/sponsor",
|
||||||
|
@ -1128,20 +1212,20 @@
|
||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2020-10-23T14:02:19+00:00"
|
"time": "2021-02-19T12:13:01+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-php72",
|
"name": "symfony/polyfill-php72",
|
||||||
"version": "v1.20.0",
|
"version": "v1.23.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/polyfill-php72.git",
|
"url": "https://github.com/symfony/polyfill-php72.git",
|
||||||
"reference": "cede45fcdfabdd6043b3592e83678e42ec69e930"
|
"reference": "9a142215a36a3888e30d0a9eeea9766764e96976"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/cede45fcdfabdd6043b3592e83678e42ec69e930",
|
"url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/9a142215a36a3888e30d0a9eeea9766764e96976",
|
||||||
"reference": "cede45fcdfabdd6043b3592e83678e42ec69e930",
|
"reference": "9a142215a36a3888e30d0a9eeea9766764e96976",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
@ -1150,7 +1234,7 @@
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"extra": {
|
"extra": {
|
||||||
"branch-alias": {
|
"branch-alias": {
|
||||||
"dev-main": "1.20-dev"
|
"dev-main": "1.23-dev"
|
||||||
},
|
},
|
||||||
"thanks": {
|
"thanks": {
|
||||||
"name": "symfony/polyfill",
|
"name": "symfony/polyfill",
|
||||||
|
@ -1187,6 +1271,9 @@
|
||||||
"portable",
|
"portable",
|
||||||
"shim"
|
"shim"
|
||||||
],
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/polyfill-php72/tree/v1.23.0"
|
||||||
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"url": "https://symfony.com/sponsor",
|
"url": "https://symfony.com/sponsor",
|
||||||
|
@ -1201,7 +1288,7 @@
|
||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2020-10-23T14:02:19+00:00"
|
"time": "2021-05-27T09:17:38+00:00"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"packages-dev": [
|
"packages-dev": [
|
||||||
|
@ -1249,6 +1336,10 @@
|
||||||
"isolation",
|
"isolation",
|
||||||
"tool"
|
"tool"
|
||||||
],
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/bamarni/composer-bin-plugin/issues",
|
||||||
|
"source": "https://github.com/bamarni/composer-bin-plugin/tree/master"
|
||||||
|
},
|
||||||
"time": "2020-05-03T08:27:20+00:00"
|
"time": "2020-05-03T08:27:20+00:00"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -1258,15 +1349,16 @@
|
||||||
"prefer-stable": false,
|
"prefer-stable": false,
|
||||||
"prefer-lowest": false,
|
"prefer-lowest": false,
|
||||||
"platform": {
|
"platform": {
|
||||||
"php": "^7.1",
|
"php": "^7.1 || ^8.0",
|
||||||
"ext-intl": "*",
|
"ext-intl": "*",
|
||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
"ext-hash": "*",
|
"ext-hash": "*",
|
||||||
|
"ext-filter": "*",
|
||||||
"ext-dom": "*"
|
"ext-dom": "*"
|
||||||
},
|
},
|
||||||
"platform-dev": [],
|
"platform-dev": [],
|
||||||
"platform-overrides": {
|
"platform-overrides": {
|
||||||
"php": "7.1.33"
|
"php": "7.1.33"
|
||||||
},
|
},
|
||||||
"plugin-api-version": "1.1.0"
|
"plugin-api-version": "2.1.0"
|
||||||
}
|
}
|
||||||
|
|
23
dist/apache.conf
vendored
23
dist/apache.conf
vendored
|
@ -1,23 +0,0 @@
|
||||||
# N.B. the unix:/var/run/php/php7.2-fpm.sock path used repeatedly below will
|
|
||||||
# vary from system to system and will be probably need to be changed
|
|
||||||
|
|
||||||
<VirtualHost *:80>
|
|
||||||
ServerName localhost
|
|
||||||
# adjust according to your installation path
|
|
||||||
DocumentRoot /usr/share/arsse/www
|
|
||||||
|
|
||||||
# adjust according to your installation path
|
|
||||||
ProxyFCGISetEnvIf "true" SCRIPT_FILENAME "/usr/share/arsse/arsse.php"
|
|
||||||
ProxyPreserveHost On
|
|
||||||
|
|
||||||
# Nextcloud News v1.2, Tiny Tiny RSS API, TT-RSS newsfeed icons
|
|
||||||
<LocationMatch "(/index\.php/apps/news/api/?.+|/tt-rss/(api|feed-icons))">
|
|
||||||
ProxyPass "unix:/var/run/php/php7.2-fpm.sock|fcgi://localhost/usr/share/arsse"
|
|
||||||
</LocationMatch>
|
|
||||||
|
|
||||||
# Nextcloud News API detection, Fever API
|
|
||||||
<LocationMatch "(/index\.php/apps/news/api/?$|/fever)">
|
|
||||||
# these locations should not be behind HTTP authentication
|
|
||||||
ProxyPass "unix:/var/run/php/php7.2-fpm.sock|fcgi://localhost/usr/share/arsse"
|
|
||||||
</LocationMatch>
|
|
||||||
</VirtualHost>
|
|
34
dist/apache/arsse-loc.conf
vendored
Normal file
34
dist/apache/arsse-loc.conf
vendored
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
# Nextcloud News protocol
|
||||||
|
<Location "/index.php/apps/news/api">
|
||||||
|
ProxyPass ${ARSSE_PROXY}
|
||||||
|
</Location>
|
||||||
|
|
||||||
|
# Tiny Tiny RSS protocol
|
||||||
|
<Location "/tt-rss/api">
|
||||||
|
ProxyPass ${ARSSE_PROXY}
|
||||||
|
</Location>
|
||||||
|
|
||||||
|
# Tiny Tiny RSS feed icons
|
||||||
|
<Location "/tt-rss/feed-icons">
|
||||||
|
ProxyPass ${ARSSE_PROXY}
|
||||||
|
</Location>
|
||||||
|
|
||||||
|
# Fever protocol
|
||||||
|
<Location "/fever">
|
||||||
|
ProxyPass ${ARSSE_PROXY}
|
||||||
|
</Location>
|
||||||
|
|
||||||
|
# Miniflux protocol
|
||||||
|
<Location "/v1">
|
||||||
|
ProxyPass ${ARSSE_PROXY}
|
||||||
|
</Location>
|
||||||
|
|
||||||
|
# Miniflux version number
|
||||||
|
<Location "/version">
|
||||||
|
ProxyPass ${ARSSE_PROXY}
|
||||||
|
</Location>
|
||||||
|
|
||||||
|
# Miniflux "health check"
|
||||||
|
<Location "/healthcheck">
|
||||||
|
ProxyPass ${ARSSE_PROXY}
|
||||||
|
</Location>
|
11
dist/apache/arsse.conf
vendored
Normal file
11
dist/apache/arsse.conf
vendored
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
DocumentRoot "/usr/share/arsse/www"
|
||||||
|
<Directory "/usr/share/arsse/www">
|
||||||
|
Require all granted
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
Define ARSSE_PROXY "unix:/var/run/php/arsse.sock|fcgi://localhost/usr/share/arsse/"
|
||||||
|
ProxyPreserveHost On
|
||||||
|
ProxyFCGISetEnvIf "true" SCRIPT_FILENAME "/usr/share/arsse/arsse.php"
|
||||||
|
ProxyFCGISetEnvIf "-n req('Authorization')" HTTP_AUTHORIZATION "%{req:Authorization}"
|
||||||
|
|
||||||
|
Include "/etc/arsse/apache/arsse-loc.conf"
|
9
dist/apache/example.conf
vendored
Normal file
9
dist/apache/example.conf
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<VirtualHost *:443>
|
||||||
|
ServerName "news.example.com"
|
||||||
|
SSLEngine On
|
||||||
|
|
||||||
|
SSLCertificateFile "/etc/letsencrypt/live/news.example.com/fullchain.pem"
|
||||||
|
SSLCertificateKeyFile "/etc/letsencrypt/live/news.example.com/privkey.pem"
|
||||||
|
|
||||||
|
Include "/etc/arsse/apache/arsse.conf"
|
||||||
|
</VirtualHost>
|
57
dist/arch/PKGBUILD
vendored
Normal file
57
dist/arch/PKGBUILD
vendored
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
# Maintainer: J. King <jking@jkingweb.ca>
|
||||||
|
pkgname="arsse"
|
||||||
|
pkgver=0.9.2
|
||||||
|
pkgrel=1
|
||||||
|
epoch=
|
||||||
|
pkgdesc="Multi-protocol RSS/Atom newsfeed synchronization server"
|
||||||
|
arch=("any")
|
||||||
|
url="https://thearsse.com/"
|
||||||
|
license=("MIT")
|
||||||
|
depends=()
|
||||||
|
makedepends=()
|
||||||
|
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")
|
||||||
|
backup=("etc/webapps/arsse/config.php"
|
||||||
|
"etc/php/php-fpm.d/arsse.conf"
|
||||||
|
"etc/webapps/arsse/nginx/example.conf"
|
||||||
|
"etc/webapps/arsse/nginx/arsse.conf"
|
||||||
|
"etc/webapps/arsse/nginx/arsse-loc.conf"
|
||||||
|
"etc/webapps/arsse/nginx/arsse-fcgi.conf"
|
||||||
|
"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")
|
||||||
|
md5sums=("SKIP")
|
||||||
|
|
||||||
|
package() {
|
||||||
|
# define runtime dependencies
|
||||||
|
depends=("php>=7.1" "php-intl>=7.1" "php-sqlite>=7.1" "php-fpm>=7.1")
|
||||||
|
# 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"
|
||||||
|
# copy requisite files
|
||||||
|
cd "$srcdir/arsse"
|
||||||
|
cp -r lib locale sql vendor www CHANGELOG UPGRADING README.md arsse.php "$pkgdir/usr/share/webapps/arsse"
|
||||||
|
cp -r manual/* "$pkgdir/usr/share/doc/arsse"
|
||||||
|
cp LICENSE AUTHORS "$pkgdir/usr/share/licenses/arsse"
|
||||||
|
cp dist/systemd/* "$pkgdir/usr/lib/systemd/system"
|
||||||
|
cp dist/sysuser.conf "$pkgdir/usr/lib/sysusers.d/arsse.conf"
|
||||||
|
cp dist/tmpfiles.conf "$pkgdir/usr/lib/tmpfiles.d/arsse.conf"
|
||||||
|
cp dist/php-fpm.conf "$pkgdir/etc/php/php-fpm.d/arsse.conf"
|
||||||
|
cp -r dist/man "$pkgdir/usr/share"
|
||||||
|
cp -r dist/nginx dist/apache config.defaults.php "$pkgdir/etc/webapps/arsse"
|
||||||
|
cd "$pkgdir"
|
||||||
|
# copy files requiring special permissions
|
||||||
|
cd "$srcdir/arsse"
|
||||||
|
install -Dm755 dist/arsse "$pkgdir/usr/bin/arsse"
|
||||||
|
install -Dm640 dist/config.php "$pkgdir/etc/webapps/arsse"
|
||||||
|
# patch generic configuration files to use Arch-specific paths and identifiers
|
||||||
|
sed -i -se 's/\/\(etc\|usr\/share\)\/arsse\//\/\1\/webapps\/arsse\//g' "$pkgdir/etc/webapps/arsse/nginx/"* "$pkgdir/etc/webapps/arsse/apache/"* "$pkgdir/usr/lib/tmpfiles.d/arsse.conf" "$pkgdir/usr/lib/systemd/system/"* "$pkgdir/usr/bin/"*
|
||||||
|
sed -i -se 's/\/var\/run\/php\//\/run\/php-fpm\//g' "$pkgdir/etc/webapps/arsse/nginx/"* "$pkgdir/etc/webapps/arsse/apache/"* "$pkgdir/etc/php/php-fpm.d/arsse.conf"
|
||||||
|
sed -i -se 's/www-data/http/g' "$pkgdir/etc/php/php-fpm.d/arsse.conf"
|
||||||
|
sed -i -e 's/^WorkingDirectory=.*$/WorkingDirectory=\/usr\/share\/webapps\/arsse/g' -e 's/^ConfigurationDirectory=.*$/ConfigurationDirectory=webapps\/arsse/g' "$pkgdir/usr/lib/systemd/system/arsse-fetch.service"
|
||||||
|
}
|
74
dist/arch/PKGBUILD-git
vendored
Normal file
74
dist/arch/PKGBUILD-git
vendored
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
# Maintainer: J. King <jking@jkingweb.ca>
|
||||||
|
pkgname="arsse-git"
|
||||||
|
pkgver=0.9.2
|
||||||
|
pkgrel=1
|
||||||
|
epoch=
|
||||||
|
pkgdesc="Multi-protocol RSS/Atom newsfeed synchronization server, bugfix-testing version"
|
||||||
|
arch=("any")
|
||||||
|
url="https://thearsse.com/"
|
||||||
|
license=("MIT")
|
||||||
|
provides=("arsse")
|
||||||
|
conflicts=("arsse")
|
||||||
|
depends=("php>=7.1" "php-intl>=7.1" "php-sqlite>=7.1")
|
||||||
|
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")
|
||||||
|
backup=("etc/webapps/arsse/config.php"
|
||||||
|
"etc/php/php-fpm.d/arsse.conf"
|
||||||
|
"etc/webapps/arsse/nginx/example.conf"
|
||||||
|
"etc/webapps/arsse/nginx/arsse.conf"
|
||||||
|
"etc/webapps/arsse/nginx/arsse-loc.conf"
|
||||||
|
"etc/webapps/arsse/nginx/arsse-fcgi.conf"
|
||||||
|
"etc/webapps/arsse/apache/example.conf"
|
||||||
|
"etc/webapps/arsse/apache/arsse.conf"
|
||||||
|
"etc/webapps/arsse/apache/arsse-loc.conf")
|
||||||
|
source=("git+https://code.mensbeam.com/MensBeam/arsse/")
|
||||||
|
md5sums=("SKIP")
|
||||||
|
|
||||||
|
pkgver() {
|
||||||
|
cd "arsse"
|
||||||
|
git describe --tags | sed 's/\([^-]*-g\)/r\1/;s/-/./g'
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
cd "$srcdir/arsse"
|
||||||
|
composer install
|
||||||
|
./robo manpage
|
||||||
|
./robo manual
|
||||||
|
composer install --no-dev -o --no-scripts
|
||||||
|
php arsse.php conf save-defaults config.defaults.php
|
||||||
|
rm -r vendor/bin
|
||||||
|
}
|
||||||
|
|
||||||
|
package() {
|
||||||
|
# define runtime dependencies
|
||||||
|
depends=("php>=7.1" "php-intl>=7.1" "php-sqlite>=7.1" "php-fpm>=7.1")
|
||||||
|
# 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"
|
||||||
|
# copy requisite files
|
||||||
|
cd "$srcdir/arsse"
|
||||||
|
cp -r lib locale sql vendor www CHANGELOG UPGRADING README.md arsse.php "$pkgdir/usr/share/webapps/arsse"
|
||||||
|
cp -r manual/* "$pkgdir/usr/share/doc/arsse"
|
||||||
|
cp LICENSE AUTHORS "$pkgdir/usr/share/licenses/arsse"
|
||||||
|
cp dist/systemd/* "$pkgdir/usr/lib/systemd/system"
|
||||||
|
cp dist/sysuser.conf "$pkgdir/usr/lib/sysusers.d/arsse.conf"
|
||||||
|
cp dist/tmpfiles.conf "$pkgdir/usr/lib/tmpfiles.d/arsse.conf"
|
||||||
|
cp dist/php-fpm.conf "$pkgdir/etc/php/php-fpm.d/arsse.conf"
|
||||||
|
cp -r dist/man "$pkgdir/usr/share"
|
||||||
|
cp -r dist/nginx dist/apache config.defaults.php "$pkgdir/etc/webapps/arsse"
|
||||||
|
cd "$pkgdir"
|
||||||
|
# copy files requiring special permissions
|
||||||
|
cd "$srcdir/arsse"
|
||||||
|
install -Dm755 dist/arsse "$pkgdir/usr/bin/arsse"
|
||||||
|
install -Dm640 dist/config.php "$pkgdir/etc/webapps/arsse"
|
||||||
|
# patch generic configuration files to use Arch-specific paths and identifiers
|
||||||
|
sed -i -se 's/\/\(etc\|usr\/share\)\/arsse\//\/\1\/webapps\/arsse\//g' "$pkgdir/etc/webapps/arsse/nginx/"* "$pkgdir/etc/webapps/arsse/apache/"* "$pkgdir/usr/lib/tmpfiles.d/arsse.conf" "$pkgdir/usr/lib/systemd/system/"* "$pkgdir/usr/bin/"*
|
||||||
|
sed -i -se 's/\/var\/run\/php\//\/run\/php-fpm\//g' "$pkgdir/etc/webapps/arsse/nginx/"* "$pkgdir/etc/webapps/arsse/apache/"* "$pkgdir/etc/php/php-fpm.d/arsse.conf"
|
||||||
|
sed -i -se 's/www-data/http/g' "$pkgdir/etc/php/php-fpm.d/arsse.conf"
|
||||||
|
sed -i -e 's/^WorkingDirectory=.*$/WorkingDirectory=\/usr\/share\/webapps\/arsse/g' -e 's/^ConfigurationDirectory=.*$/ConfigurationDirectory=webapps\/arsse/g' "$pkgdir/usr/lib/systemd/system/arsse-fetch.service"
|
||||||
|
}
|
10
dist/arsse
vendored
Normal file
10
dist/arsse
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
#! /usr/bin/env php
|
||||||
|
<?php
|
||||||
|
if (posix_geteuid() == 0) {
|
||||||
|
$info = posix_getpwnam("arsse");
|
||||||
|
if ($info) {
|
||||||
|
posix_setgid($info['gid']);
|
||||||
|
posix_setuid($info['uid']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require "/usr/share/arsse/arsse.php";
|
15
dist/arsse.service
vendored
15
dist/arsse.service
vendored
|
@ -1,15 +0,0 @@
|
||||||
[Unit]
|
|
||||||
Description=The Arsse feed fetching service
|
|
||||||
After=network.target mysql.service postgresql.service
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
User=www-data
|
|
||||||
Group=www-data
|
|
||||||
WorkingDirectory=/usr/share/arsse
|
|
||||||
Type=simple
|
|
||||||
StandardOutput=null
|
|
||||||
StandardError=syslog
|
|
||||||
ExecStart=/usr/bin/env php /usr/share/arsse/arsse.php daemon
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
8
dist/config.php
vendored
Normal file
8
dist/config.php
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
# Please refer to config.defaults.php or the manual at /usr/share/doc/arsse/
|
||||||
|
# for possible configuration parameters
|
||||||
|
|
||||||
|
return [
|
||||||
|
'dbSQLite3File' => "/var/lib/arsse/arsse.db",
|
||||||
|
];
|
16
dist/debian/arsse.config
vendored
Normal file
16
dist/debian/arsse.config
vendored
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
. /usr/share/debconf/confmodule
|
||||||
|
|
||||||
|
# Set up dbconfig-common
|
||||||
|
if test -f /usr/share/dbconfig-common/dpkg/config; then
|
||||||
|
. /usr/share/dbconfig-common/dpkg/config
|
||||||
|
dbc_dbtypes="sqlite3, pgsql, mysql"
|
||||||
|
dbc_authmethod_user="password"
|
||||||
|
dbc_go arsse "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prompt for dbconfig-common configuration
|
||||||
|
db_go || true
|
1
dist/debian/arsse.dirs
vendored
Normal file
1
dist/debian/arsse.dirs
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
var/lib/arsse
|
18
dist/debian/arsse.install
vendored
Normal file
18
dist/debian/arsse.install
vendored
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
lib usr/share/arsse/
|
||||||
|
locale usr/share/arsse/
|
||||||
|
sql usr/share/arsse/
|
||||||
|
vendor usr/share/arsse/
|
||||||
|
www usr/share/arsse/
|
||||||
|
CHANGELOG usr/share/arsse/
|
||||||
|
UPGRADING usr/share/arsse/
|
||||||
|
README.md usr/share/arsse/
|
||||||
|
arsse.php usr/share/arsse/
|
||||||
|
|
||||||
|
config.defaults.php etc/arsse/
|
||||||
|
manual usr/share/doc/arsse/
|
||||||
|
dist/man/* usr/share/man/
|
||||||
|
dist/debian/config.php etc/arsse/
|
||||||
|
dist/debian/dbconfig-common.php usr/share/arsse/
|
||||||
|
debian/bin/arsse usr/bin/
|
||||||
|
debian/nginx etc/arsse/
|
||||||
|
debian/apache etc/arsse/
|
1
dist/debian/arsse.links
vendored
Normal file
1
dist/debian/arsse.links
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
etc/arsse/config.php usr/share/arsse/config.php
|
29
dist/debian/arsse.postinst
vendored
Normal file
29
dist/debian/arsse.postinst
vendored
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
. /usr/share/debconf/confmodule
|
||||||
|
|
||||||
|
if [ "$1" = "configure" ]; then
|
||||||
|
# Set permissions on configuration file
|
||||||
|
dpkg-statoverride --list "/etc/arsse/config.php" >/dev/null || dpkg-statoverride --update --add root www-data 640 "/etc/arsse/config.php"
|
||||||
|
# Set up dbconfig-common
|
||||||
|
if test -f /usr/share/dbconfig-common/dpkg/postinst; then
|
||||||
|
. /usr/share/dbconfig-common/dpkg/postinst
|
||||||
|
dbc_generate_include_owner="root:www-data"
|
||||||
|
dbc_generate_include_perms="0640"
|
||||||
|
dbc_generate_include="php:/var/lib/arsse/dbconfig.inc"
|
||||||
|
dbc_pgsql_createdb_encoding="UTF8' lc_collate='C"
|
||||||
|
dbc_mysql_createdb_encoding="UTF8"
|
||||||
|
dbc_dbfile_owner="root:www-data"
|
||||||
|
dbc_dbfile_perms="0660"
|
||||||
|
dbc_go arsse "$@"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# dh_installdeb will replace this with shell code automatically
|
||||||
|
# generated by other debhelper scripts.
|
||||||
|
|
||||||
|
#DEBHELPER#
|
||||||
|
|
||||||
|
exit 0
|
20
dist/debian/arsse.postrm
vendored
Normal file
20
dist/debian/arsse.postrm
vendored
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if test -f /usr/share/debconf/confmodule; then
|
||||||
|
. /usr/share/debconf/confmodule
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set up dbconfig-common
|
||||||
|
if test -f /usr/share/dbconfig-common/dpkg/postrm; then
|
||||||
|
. /usr/share/dbconfig-common/dpkg/postrm
|
||||||
|
dbc_go arsse "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# dh_installdeb will replace this with shell code automatically
|
||||||
|
# generated by other debhelper scripts.
|
||||||
|
|
||||||
|
#DEBHELPER#
|
||||||
|
|
||||||
|
exit 0
|
16
dist/debian/arsse.prerm
vendored
Normal file
16
dist/debian/arsse.prerm
vendored
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
. /usr/share/debconf/confmodule
|
||||||
|
|
||||||
|
# Set up dbconfig-common
|
||||||
|
. /usr/share/dbconfig-common/dpkg/prerm
|
||||||
|
dbc_go arsse "$@"
|
||||||
|
|
||||||
|
# dh_installdeb will replace this with shell code automatically
|
||||||
|
# generated by other debhelper scripts.
|
||||||
|
|
||||||
|
#DEBHELPER#
|
||||||
|
|
||||||
|
exit 0
|
1
dist/debian/compat
vendored
Normal file
1
dist/debian/compat
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
10
|
15
dist/debian/config.php
vendored
Normal file
15
dist/debian/config.php
vendored
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<?php
|
||||||
|
/***
|
||||||
|
Please refer to config.defaults.php or the manual at /usr/share/doc/arsse/
|
||||||
|
for possible configuration parameters.
|
||||||
|
|
||||||
|
The last line includes database auto-configuration information which
|
||||||
|
Debian may have created during installation; any database-related
|
||||||
|
configuration defined in this file will override anything defined in the
|
||||||
|
included file.
|
||||||
|
***/
|
||||||
|
|
||||||
|
return [
|
||||||
|
'dbAutoUpdate' => true,
|
||||||
|
]
|
||||||
|
+ (@include "/usr/share/arsse/dbconfig-common.php");
|
32
dist/debian/control
vendored
Normal file
32
dist/debian/control
vendored
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
Source: arsse
|
||||||
|
Maintainer: J. King <jking@jkingweb.ca>
|
||||||
|
Section: contrib/net
|
||||||
|
Priority: optional
|
||||||
|
Standards-Version: 4.5.1
|
||||||
|
Homepage: https://thearsse.com/
|
||||||
|
Vcs-Browser: https://code.mensbeam.com/MensBeam/arsse/
|
||||||
|
Vcs-Git: https://code.mensbeam.com/MensBeam/arsse/
|
||||||
|
Build-Depends: debhelper
|
||||||
|
|
||||||
|
Package: arsse
|
||||||
|
Architecture: all
|
||||||
|
Section: contrib/net
|
||||||
|
Priority: optional
|
||||||
|
Homepage: https://thearsse.com/
|
||||||
|
Description: Multi-protocol RSS/Atom newsfeed synchronization server
|
||||||
|
The Arsse bridges the gap between multiple existing newsfeed aggregator
|
||||||
|
client protocols such as Tiny Tiny RSS, Nextcloud News and Miniflux,
|
||||||
|
allowing you to use compatible clients for many protocols with a single
|
||||||
|
server.
|
||||||
|
Depends: ${misc:Depends},
|
||||||
|
dbconfig-sqlite3 | dbconfig-pgsql | dbconfig-mysql | dbconfig-no-thanks,
|
||||||
|
php (>= 7.1.0),
|
||||||
|
php-cli,
|
||||||
|
php-intl,
|
||||||
|
php-json,
|
||||||
|
php-xml,
|
||||||
|
php-sqlite3 | php-pgsql | php-mysql
|
||||||
|
Recommends: nginx | apache2,
|
||||||
|
php-fpm,
|
||||||
|
php-curl,
|
||||||
|
ca-certificates
|
34
dist/debian/copyright
vendored
Normal file
34
dist/debian/copyright
vendored
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||||
|
Upstream-Name: arsse
|
||||||
|
Upstream-Contact: J. King <jking@jkingweb.ca>
|
||||||
|
Source: https://code.mensbeam.com/MensBeam/arsse/
|
||||||
|
License: Expat
|
||||||
|
|
||||||
|
Files: *
|
||||||
|
Copyright: 2017 J. King <jking@jkingweb.ca>
|
||||||
|
2017 Dustin Wilson <dustin@dustinwilson.com>
|
||||||
|
License: Expat
|
||||||
|
|
||||||
|
License: Expat
|
||||||
|
Copyright (c) 2017 J. King, Dustin Wilson
|
||||||
|
.
|
||||||
|
Permission is hereby granted, free of charge, to any person
|
||||||
|
obtaining a copy of this software and associated documentation
|
||||||
|
files (the "Software"), to deal in the Software without
|
||||||
|
restriction, including without limitation the rights to use,
|
||||||
|
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the
|
||||||
|
Software is furnished to do so, subject to the following
|
||||||
|
conditions:
|
||||||
|
.
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
.
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||||
|
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||||
|
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
|
OTHER DEALINGS IN THE SOFTWARE.
|
44
dist/debian/dbconfig-common.php
vendored
Normal file
44
dist/debian/dbconfig-common.php
vendored
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
# This script transforms Debian's dbconfig-common PHP-format include files
|
||||||
|
# into a form usable by The Arsse. This is necessary because The Arsse
|
||||||
|
# supports defining configuration parameters for all supported database types
|
||||||
|
# at once, using separate keys for the different types
|
||||||
|
|
||||||
|
$dbconfpath = "/var/lib/arsse/dbconfig.inc"; // path defined in postinst script
|
||||||
|
|
||||||
|
if (file_exists($dbconfpath)) {
|
||||||
|
require_once "/var/lib/arsse/dbconfig.inc";
|
||||||
|
$dbtype = $dbtype ?? "";
|
||||||
|
// the returned configuration depends on the $dbtype
|
||||||
|
if ($dbtype === "sqlite3") {
|
||||||
|
$conf = ['dbDriver' => "sqlite3"];
|
||||||
|
if (strlen((string) $basepath) && strlen((string) $dbname)) {
|
||||||
|
$conf['dbSQLite3File'] = "$basepath/$dbname";
|
||||||
|
}
|
||||||
|
} elseif ($dbtype === "pgsql") {
|
||||||
|
$conf = [
|
||||||
|
'dbDriver' => "postgresql",
|
||||||
|
'dbPostgreSQLHost' => $dbserver ?? "",
|
||||||
|
'dbPostgreSQLUser' => $dbuser ?? "arsse",
|
||||||
|
'dbPostgreSQLPass' => $dbpass ?? "",
|
||||||
|
'dbPostgreSQLPort' => (int) $dbport ?: 5432,
|
||||||
|
'dbPostgreSQLDb' => $dbname ?? "arsse",
|
||||||
|
];
|
||||||
|
} elseif ($dbtype === "mysql") {
|
||||||
|
$conf = [
|
||||||
|
'dbDriver' => "mysql",
|
||||||
|
'dbMySQLHost' => $dbserver ?? "",
|
||||||
|
'dbMySQLUser' => $dbuser ?? "arsse",
|
||||||
|
'dbMySQLPass' => $dbpass ?? "",
|
||||||
|
'dbMySQLPort' => (int) $dbport ?: 3306,
|
||||||
|
'dbMySQLDb' => $dbname ?? "arsse",
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
throw new \Exception("Debian dbconfig-common configuration file $dbconfpath is invalid");
|
||||||
|
}
|
||||||
|
return $conf;
|
||||||
|
} else {
|
||||||
|
// if no configuration file exists simply return an empty array
|
||||||
|
return [];
|
||||||
|
}
|
6
dist/debian/lintian-overrides
vendored
Normal file
6
dist/debian/lintian-overrides
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
# We make reference to "Tiny Tiny RSS"
|
||||||
|
spelling-error-in-description Tiny Tiny (duplicate word) Tiny
|
||||||
|
# The manual for DrUUID (a dependency) includes a harmless "up" link
|
||||||
|
privacy-breach-generic usr/share/arsse/vendor/jkingweb/druuid/documentation/manual.html [<link rel="up" href="http://jkingweb.ca/code/">] (http://jkingweb.ca/code/)
|
||||||
|
# We only ask dbconfig-common questions, which don't seem to require templates
|
||||||
|
no-debconf-templates
|
40
dist/debian/pbuilder.sh
vendored
Executable file
40
dist/debian/pbuilder.sh
vendored
Executable file
|
@ -0,0 +1,40 @@
|
||||||
|
#! /bin/bash -e
|
||||||
|
|
||||||
|
###
|
||||||
|
# This script is fed to pbuilder to build Debian packages. The base tarball
|
||||||
|
# should be created with a command similar to the following:
|
||||||
|
#
|
||||||
|
# sudo pbuilder create --basetgz pbuilder-arsse.tgz --mirror http://ftp.ca.debian.org/debian/ --extrapackages "debhelper devscripts lintian"
|
||||||
|
#
|
||||||
|
# Thereafter pbuilder can be used to build packages with this command:
|
||||||
|
#
|
||||||
|
# sudo pbuilder execute --basetgz pbuilder-arsse.tgz --bindmounts `basedir "/path/to/release/tarball"` -- pbuilder.sh "/path/to/release/tarball"
|
||||||
|
#
|
||||||
|
# This somewhat roundabout procedure is used because the pbuilder debuild
|
||||||
|
# command does not seem to work in Arch Linux, nor does pdebuild. Doing
|
||||||
|
# as much as possible within the chroot itself works around these problems.
|
||||||
|
###
|
||||||
|
|
||||||
|
# create a temporary directory
|
||||||
|
tmp=`mktemp -d`
|
||||||
|
|
||||||
|
# define various variables
|
||||||
|
here=`dirname "$1"`
|
||||||
|
tarball=`basename "$1"`
|
||||||
|
version=`echo "$tarball" | grep -oP '\d+(?:\.\d+)*' | head -1`
|
||||||
|
out="$here/debian"
|
||||||
|
in="$tmp/arsse-$version"
|
||||||
|
|
||||||
|
# create necessary directories
|
||||||
|
mkdir -p "$in" "$out"
|
||||||
|
# extract the release tarball
|
||||||
|
tar -C "$in" -xf "$1" --strip-components=1
|
||||||
|
# repackage the release tarball into a Debian "orig" tarball
|
||||||
|
tar -C "$tmp" -czf "$tmp/arsse_$version.orig.tar.gz" "arsse-$version"
|
||||||
|
# copy the "dist/debian" directory down the tree where Debian expects it
|
||||||
|
cp -r "$in/dist/debian" "$in/debian"
|
||||||
|
# build the package
|
||||||
|
cd "$in"
|
||||||
|
debuild -us -uc
|
||||||
|
# move the resultant files to their final destination
|
||||||
|
find "$tmp" -maxdepth 1 -type f -exec mv '{}' "$out" \;
|
26
dist/debian/rules
vendored
Executable file
26
dist/debian/rules
vendored
Executable file
|
@ -0,0 +1,26 @@
|
||||||
|
#!/usr/bin/make -f
|
||||||
|
|
||||||
|
DH_VERBOSE = 1
|
||||||
|
|
||||||
|
%:
|
||||||
|
dh $@
|
||||||
|
|
||||||
|
execute_before_dh_install:
|
||||||
|
# Adapt the systemd service for Debian: this involves using only the "arsse-fetch" unit (renamed to "arsse"), removing the "PartOf" directive, and changing the user and group to "www-data"
|
||||||
|
cp dist/systemd/arsse-fetch.service debian/arsse.service
|
||||||
|
sed -i -se 's/^PartOf=.*//' debian/arsse.service
|
||||||
|
sed -i -se 's/^\(User\|Group\)=.*/\1=www-data/' debian/arsse.service
|
||||||
|
# Adapt the init script for Debian: this involves changing the user and group to "www-data"
|
||||||
|
cp dist/init.sh debian/arsse.init
|
||||||
|
sed -i -se 's/^\([ \t]*chown\) arsse:arsse /\1 www-data:www-data /' debian/arsse.init
|
||||||
|
# Change the user and group references in tmpfiles
|
||||||
|
cp dist/tmpfiles.conf debian/arsse.tmpfiles
|
||||||
|
sed -i -se 's/ arsse / www-data /' debian/arsse.tmpfiles
|
||||||
|
sed -i -se 's/ arsse / www-data /' debian/arsse.tmpfiles
|
||||||
|
# Change the user reference in the executable file
|
||||||
|
mkdir -p debian/bin
|
||||||
|
cp dist/arsse debian/bin/arsse
|
||||||
|
sed -i -se 's/posix_getpwnam("arsse"/posix_getpwnam("www-data"/' debian/bin/arsse
|
||||||
|
# Change PHP-FPM socket paths
|
||||||
|
cp -r dist/apache dist/nginx debian
|
||||||
|
sed -i -se 's/arsse\.sock/php-fpm.sock/' debian/apache/arsse.conf debian/nginx/arsse.conf
|
1
dist/debian/source/format
vendored
Normal file
1
dist/debian/source/format
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
3.0 (quilt)
|
2
dist/debian/source/lintian-overrides
vendored
Normal file
2
dist/debian/source/lintian-overrides
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
# Development environment is slightly out of date
|
||||||
|
newer-standards-version
|
78
dist/init.sh
vendored
Normal file
78
dist/init.sh
vendored
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
### BEGIN INIT INFO
|
||||||
|
# Provides: arsse
|
||||||
|
# Required-Start: $local_fs $network
|
||||||
|
# Required-Stop: $local_fs postgresql mysql
|
||||||
|
# Should-Start: postgresql mysql
|
||||||
|
# Should-Stop: postgresql mysql
|
||||||
|
# Default-Start: 2 3 4 5
|
||||||
|
# Default-Stop: 0 1 6
|
||||||
|
# Short-Description: The Advanced RSS Environment
|
||||||
|
# Description: The Arsse is a multi-protocol Web newsfeed synchronization service
|
||||||
|
### END INIT INFO
|
||||||
|
|
||||||
|
# This script is designed for Debian; some adaptation will be required for other systems
|
||||||
|
|
||||||
|
PATH=/usr/sbin/:/usr/bin:/sbin:/bin
|
||||||
|
NAME=arsse
|
||||||
|
DESC="newsfeed synchronization server"
|
||||||
|
PIDFILE=/run/arsse.pid
|
||||||
|
DAEMON=/usr/bin/$NAME
|
||||||
|
|
||||||
|
. /lib/init/vars.sh
|
||||||
|
. /lib/lsb/init-functions
|
||||||
|
|
||||||
|
arsse_start() {
|
||||||
|
touch "$PIDFILE"
|
||||||
|
chown arsse:arsse "$PIDFILE"
|
||||||
|
$DAEMON daemon --fork="$PIDFILE" || return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
arsse_stop() {
|
||||||
|
killproc -p "$PIDFILE" "$DAEMON"
|
||||||
|
}
|
||||||
|
|
||||||
|
arsse_reload() {
|
||||||
|
killproc -p "$PIDFILE" "$DAEMON" HUP
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
start)
|
||||||
|
log_daemon_msg "Starting $DESC" "$NAME"
|
||||||
|
if pidofproc -p $PIDFILE "$DAEMON" > /dev/null 2>&1 ; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
arsse_start
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
log_daemon_msg "Stopping $DESC" "$NAME"
|
||||||
|
arsse_stop
|
||||||
|
;;
|
||||||
|
restart)
|
||||||
|
log_daemon_msg "Restarting $DESC" "$NAME"
|
||||||
|
if pidofproc -p $PIDFILE "$DAEMON" > /dev/null 2>&1 ; then
|
||||||
|
arsse_stop
|
||||||
|
fi
|
||||||
|
arsse_start
|
||||||
|
;;
|
||||||
|
try-restart)
|
||||||
|
if pidofproc -p $PIDFILE "$DAEMON" > /dev/null 2>&1 ; then
|
||||||
|
log_daemon_msg "Restarting $DESC" "$NAME"
|
||||||
|
arsse_stop
|
||||||
|
arsse_start
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
reload|force-reload)
|
||||||
|
log_daemon_msg "Reloading $DESC" "$NAME"
|
||||||
|
arsse_reload
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
status_of_proc -p $PIDFILE $DAEMON $NAME
|
||||||
|
exit $?
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Usage: $0 {start|stop|restart|try-restart|reload|status}" >&2
|
||||||
|
exit 3
|
||||||
|
;;
|
||||||
|
esac
|
58
dist/nginx.conf
vendored
58
dist/nginx.conf
vendored
|
@ -1,58 +0,0 @@
|
||||||
server {
|
|
||||||
server_name example.com;
|
|
||||||
listen 80; # adding HTTPS configuration is highly recommended
|
|
||||||
root /usr/share/arsse/www; # adjust according to your installation path
|
|
||||||
|
|
||||||
location / {
|
|
||||||
try_files $uri $uri/ =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
location @arsse {
|
|
||||||
fastcgi_pass unix:/var/run/php/php7.2-fpm.sock; # adjust according to your system configuration
|
|
||||||
fastcgi_pass_header Authorization; # required if the Arsse is to perform its own HTTP authentication
|
|
||||||
fastcgi_pass_request_body on;
|
|
||||||
fastcgi_pass_request_headers on;
|
|
||||||
fastcgi_intercept_errors off;
|
|
||||||
fastcgi_buffering off;
|
|
||||||
fastcgi_param SCRIPT_FILENAME /usr/share/arsse/arsse.php; # adjust according to your installation path
|
|
||||||
fastcgi_param REQUEST_METHOD $request_method;
|
|
||||||
fastcgi_param CONTENT_TYPE $content_type;
|
|
||||||
fastcgi_param CONTENT_LENGTH $content_length;
|
|
||||||
fastcgi_param REQUEST_URI $uri;
|
|
||||||
fastcgi_param QUERY_STRING $query_string;
|
|
||||||
fastcgi_param HTTPS $https if_not_empty;
|
|
||||||
fastcgi_param REMOTE_USER $remote_user;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Nextcloud News protocol
|
|
||||||
location /index.php/apps/news/api {
|
|
||||||
try_files $uri @arsse;
|
|
||||||
|
|
||||||
location ~ ^/index\.php/apps/news/api/?$ {
|
|
||||||
# this path should not be behind HTTP authentication
|
|
||||||
try_files $uri @arsse;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Tiny Tiny RSS protocol
|
|
||||||
location /tt-rss/api {
|
|
||||||
try_files $uri @arsse;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Tiny Tiny RSS feed icons
|
|
||||||
location /tt-rss/feed-icons/ {
|
|
||||||
try_files $uri @arsse;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Tiny Tiny RSS special-feed icons; these are static files
|
|
||||||
location /tt-rss/images/ {
|
|
||||||
# this path should not be behind HTTP authentication
|
|
||||||
try_files $uri =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Fever protocol
|
|
||||||
location /fever/ {
|
|
||||||
# this path should not be behind HTTP authentication
|
|
||||||
try_files $uri @arsse;
|
|
||||||
}
|
|
||||||
}
|
|
12
dist/nginx/arsse-fcgi.conf
vendored
Normal file
12
dist/nginx/arsse-fcgi.conf
vendored
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
fastcgi_pass_header Authorization; # required if the Arsse is to perform its own HTTP authentication
|
||||||
|
fastcgi_pass_request_body on;
|
||||||
|
fastcgi_pass_request_headers on;
|
||||||
|
fastcgi_intercept_errors off;
|
||||||
|
fastcgi_buffering off;
|
||||||
|
fastcgi_param REQUEST_METHOD $request_method;
|
||||||
|
fastcgi_param CONTENT_TYPE $content_type;
|
||||||
|
fastcgi_param CONTENT_LENGTH $content_length;
|
||||||
|
fastcgi_param REQUEST_URI $uri;
|
||||||
|
fastcgi_param QUERY_STRING $query_string;
|
||||||
|
fastcgi_param HTTPS $https if_not_empty;
|
||||||
|
fastcgi_param REMOTE_USER $remote_user;
|
49
dist/nginx/arsse-loc.conf
vendored
Normal file
49
dist/nginx/arsse-loc.conf
vendored
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
# Any provided static files
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Nextcloud News protocol
|
||||||
|
location /index.php/apps/news/api {
|
||||||
|
try_files $uri @arsse;
|
||||||
|
|
||||||
|
location ~ ^/index\.php/apps/news/api/?$ {
|
||||||
|
try_files $uri @arsse_public;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Tiny Tiny RSS protocol
|
||||||
|
location /tt-rss/api {
|
||||||
|
try_files $uri @arsse;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Tiny Tiny RSS feed icons
|
||||||
|
location /tt-rss/feed-icons/ {
|
||||||
|
try_files $uri @arsse;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Tiny Tiny RSS special-feed icons; these are static files
|
||||||
|
location /tt-rss/images/ {
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fever protocol
|
||||||
|
location /fever/ {
|
||||||
|
try_files $uri @arsse;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Miniflux protocol
|
||||||
|
location /v1/ {
|
||||||
|
# If put behind HTTP authentication token login will not be possible
|
||||||
|
try_files $uri @arsse;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Miniflux version number
|
||||||
|
location /version {
|
||||||
|
try_files $uri @arsse_public;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Miniflux "health check"
|
||||||
|
location /healthcheck {
|
||||||
|
try_files $uri @arsse_public;
|
||||||
|
}
|
17
dist/nginx/arsse.conf
vendored
Normal file
17
dist/nginx/arsse.conf
vendored
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
root /usr/share/arsse/www;
|
||||||
|
|
||||||
|
location @arsse {
|
||||||
|
# HTTP authentication may be enabled for this location, though this may impact some features
|
||||||
|
fastcgi_pass unix:/var/run/php/arsse.sock;
|
||||||
|
fastcgi_param SCRIPT_FILENAME /usr/share/arsse/arsse.php;
|
||||||
|
include /etc/arsse/nginx/arsse-fcgi.conf;
|
||||||
|
}
|
||||||
|
|
||||||
|
location @arsse_public {
|
||||||
|
# HTTP authentication should not be enabled for this location
|
||||||
|
fastcgi_pass unix:/var/run/php/arsse.sock;
|
||||||
|
fastcgi_param SCRIPT_FILENAME /usr/share/arsse/arsse.php;
|
||||||
|
include /etc/arsse/nginx/arsse-fcgi.conf;
|
||||||
|
}
|
||||||
|
|
||||||
|
include /etc/arsse/nginx/arsse-loc.conf;
|
13
dist/nginx/example.conf
vendored
Normal file
13
dist/nginx/example.conf
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
server {
|
||||||
|
server_name news.example.com;
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
listen 443 ssl http2;
|
||||||
|
listen [::]:443 ssl http2;
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/news.example.com/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/news.example.com/privkey.pem;
|
||||||
|
ssl_trusted_certificate /etc/letsencrypt/live/news.example.com/chain.pem;
|
||||||
|
|
||||||
|
include /etc/arsse/nginx/arsse.conf;
|
||||||
|
}
|
11
dist/php-fpm.conf
vendored
Normal file
11
dist/php-fpm.conf
vendored
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
[arsse]
|
||||||
|
user = arsse
|
||||||
|
group = arsse
|
||||||
|
listen = /var/run/php/arsse.sock
|
||||||
|
listen.owner = arsse
|
||||||
|
listen.group = www-data
|
||||||
|
pm = dynamic
|
||||||
|
pm.max_children = 5
|
||||||
|
pm.start_servers = 2
|
||||||
|
pm.min_spare_servers = 1
|
||||||
|
pm.max_spare_servers = 3
|
36
dist/systemd/arsse-fetch.service
vendored
Normal file
36
dist/systemd/arsse-fetch.service
vendored
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
[Unit]
|
||||||
|
Description=The Arsse newsfeed fetching service
|
||||||
|
Documentation=https://thearsse.com/manual/
|
||||||
|
PartOf=arsse.service
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=arsse
|
||||||
|
Group=arsse
|
||||||
|
Type=simple
|
||||||
|
WorkingDirectory=/usr/share/arsse
|
||||||
|
ExecStart=/usr/bin/arsse daemon
|
||||||
|
|
||||||
|
ProtectProc=invisible
|
||||||
|
NoNewPrivileges=true
|
||||||
|
ProtectSystem=full
|
||||||
|
ProtectHome=true
|
||||||
|
StateDirectory=arsse
|
||||||
|
ConfigurationDirectory=arsse
|
||||||
|
PrivateTmp=true
|
||||||
|
PrivateDevices=true
|
||||||
|
RestrictSUIDSGID=true
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier=arsse
|
||||||
|
Restart=on-failure
|
||||||
|
RestartPreventStatus=
|
||||||
|
|
||||||
|
# These directives can be used for extra security, but are disabled for now for compatibility
|
||||||
|
|
||||||
|
#ReadOnlyPaths=/
|
||||||
|
#ReadWriePaths=/var/lib/arsse
|
||||||
|
#NoExecPaths=/
|
||||||
|
#ExecPaths=/usr/bin/php
|
14
dist/systemd/arsse.service
vendored
Normal file
14
dist/systemd/arsse.service
vendored
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
[Unit]
|
||||||
|
Description=The Arsse newsfeed management service
|
||||||
|
Documentation=https://thearsse.com/manual/
|
||||||
|
Requires=arsse-fetch.service
|
||||||
|
BindsTo=php-fpm.service
|
||||||
|
After=php-fpm.service
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
RemainAfterExit=true
|
||||||
|
ExecStart=/usr/bin/true
|
1
dist/sysuser.conf
vendored
Normal file
1
dist/sysuser.conf
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
u arsse - "The Arsse" /var/lib/arsse -
|
4
dist/tmpfiles.conf
vendored
Normal file
4
dist/tmpfiles.conf
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
z /usr/bin/arsse 0755 root arsse - -
|
||||||
|
z /etc/arsse/config.php 0640 root arsse - -
|
||||||
|
L /usr/share/arsse/config.php - root arsse - /etc/arsse/config.php
|
||||||
|
d /var/lib/arsse 0750 arsse arsse - -
|
|
@ -1,5 +1,6 @@
|
||||||
The Advanced RSS Environment (affectionately called "The Arsse") is a news aggregator server which implements multiple synchronization protocols. Unlike most other aggregator servers, The Arsse does not include a Web front-end (though one is planned as a separate project), and it relies on [existing protocols](Supported_Protocols) to maximize compatibility with [existing clients](Compatible_Clients). Supported protocols are:
|
The Advanced RSS Environment (affectionately called "The Arsse") is a news aggregator server which implements multiple synchronization protocols. Unlike most other aggregator servers, The Arsse does not include a Web front-end (though one is planned as a separate project), and it relies on [existing protocols](Supported_Protocols) to maximize compatibility with [existing clients](Compatible_Clients). Supported protocols are:
|
||||||
|
|
||||||
|
- Miniflux
|
||||||
- Nextcloud News
|
- Nextcloud News
|
||||||
- Tiny Tiny RSS
|
- Tiny Tiny RSS
|
||||||
- Fever
|
- Fever
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
The Arsse has the following requirements:
|
|
||||||
|
|
||||||
- A Linux server running Nginx or Apache 2.4
|
|
||||||
- PHP 7.1.0 or later with the following extensions:
|
|
||||||
- [intl](http://php.net/manual/en/book.intl.php), [json](http://php.net/manual/en/book.json.php), [hash](http://php.net/manual/en/book.hash.php), and [dom](http://php.net/manual/en/book.dom.php)
|
|
||||||
- [simplexml](http://php.net/manual/en/book.simplexml.php), and [iconv](http://php.net/manual/en/book.iconv.php)
|
|
||||||
- One of:
|
|
||||||
- [sqlite3](http://php.net/manual/en/book.sqlite3.php) or [pdo_sqlite](http://php.net/manual/en/ref.pdo-sqlite.php) for SQLite databases
|
|
||||||
- [pgsql](http://php.net/manual/en/book.pgsql.php) or [pdo_pgsql](http://php.net/manual/en/ref.pdo-pgsql.php) for PostgreSQL 10 or later databases
|
|
||||||
- [mysqli](http://php.net/manual/en/book.mysqli.php) or [pdo_mysql](http://php.net/manual/en/ref.pdo-mysql.php) for MySQL/Percona 8.0.11 or later databases
|
|
||||||
- [curl](http://php.net/manual/en/book.curl.php) (optional)
|
|
||||||
- Privileges either to create and run systemd services, or to run cron jobs
|
|
||||||
|
|
||||||
Instructions for how to satisfy the PHP extension requirements for Debian systems are included in the next section.
|
|
||||||
|
|
||||||
It is also be possible to run The Arsse on other operating systems (including Windows) and with other Web servers, but the configuration required to do so is not documented in this manual.
|
|
|
@ -1,41 +0,0 @@
|
||||||
[TOC]
|
|
||||||
|
|
||||||
# Downloading The Arse
|
|
||||||
|
|
||||||
The latest version of The Arsse can be downloaded [from our Web site](https://thearsse.com/). If installing an older release from our archives, the attachments named _arsse-x.x.x.tar.gz_ should be used rather than those marked "Source Code".
|
|
||||||
|
|
||||||
Installation from source code is also possible, but the release packages are recommended.
|
|
||||||
|
|
||||||
# Installation
|
|
||||||
|
|
||||||
In order for The Arsse to function correctly, [its requirements](Requirements) must first be satisfied. The process of installing the required PHP extensions differs from one system to the next, but on Debian the following series of commands should do:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Install PHP; this assumes the FastCGI process manager will be used
|
|
||||||
sudo apt install php-cli php-fpm
|
|
||||||
# Install the needed PHP extensions; php-curl is optional
|
|
||||||
sudo apt install php-intl php-json php-xml php-curl
|
|
||||||
# Install any one of the required database extensions
|
|
||||||
sudo apt install php-sqlite3 php-pgsql php-mysql
|
|
||||||
```
|
|
||||||
|
|
||||||
Then, it's a simple matter of unpacking the archive someplace (`/usr/share/arsse` is the recommended location on Debian systems, but it can be anywhere) and setting permissions:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Unpack the archive
|
|
||||||
sudo tar -xzf arsse-x.x.x.tar.gz -C "/usr/share"
|
|
||||||
# Make the user running the Web server the owner of the files
|
|
||||||
sudo chown -R www-data:www-data "/usr/share/arsse"
|
|
||||||
# Ensure the owner can create files such as the SQLite database
|
|
||||||
sudo chmod o+rwX "/usr/share/arsse"
|
|
||||||
```
|
|
||||||
|
|
||||||
# Next steps
|
|
||||||
|
|
||||||
If using a database other than SQLite, you will likely want to [set it up](Database_Setup) before doing anything else.
|
|
||||||
|
|
||||||
In order for the various synchronization protocols to work, a Web server [must be configured](Web_Server_Configuration), and in order for The Arsse to serve users, those users [must be created](/en/Using_The_Arsse/Managing_Users).
|
|
||||||
|
|
||||||
You may also want to review the `config.defaults.php` file included in the download package and create [a configuration file](Configuration), though The Arsse can function even without using a configuration file.
|
|
||||||
|
|
||||||
Finally, The Arsse's [newsfeed refreshing service](/en/Using_The_Arsse/Keeping_Newsfeeds_Up_to_Date) needs to be installed in order for news to actually be fetched from the Internet.
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
[TOC]
|
||||||
|
|
||||||
|
# Downloading The Arsse
|
||||||
|
|
||||||
|
Since version 0.9.2 The Arsse is available from the [Arch User Repository](https://aur.archlinux.org/) as packages `arsse` and `arsse-git`. The latter should normally only be used to test bug fixes.
|
||||||
|
|
||||||
|
Generic release tarballs may also be downloaded [from our Web site](https://thearsse.com), and the `PKGBUILD` file (found under `arsse/dist/arch/`) can then be extracted alongside the tarball and used to build the `arsse` package. Installing directly from the generic release tarball without producing an Arch package is not recommended as the package-building process performs various adjustments to handle Arch peculiarities.
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
|
||||||
|
For illustrative purposes, this document assumes the `yay` [AUR helper](https://wiki.archlinux.org/title/AUR_helpers) will be used to download, build, and install The Arsse. This section summarises the steps necessary to configure and use The Arsse after installtion:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Install the package
|
||||||
|
sudo yay -S arsse
|
||||||
|
# Enable the necessary PHP extensions; curl is optional but recommended; pdo_sqlite may be used instead of sqlite3, but this is not recommended
|
||||||
|
sudo sed -i -e 's/^;\(extension=\(curl\|iconv\|intl\|sqlite3\)\)$/\1/' /etc/php/php.ini
|
||||||
|
# Enable and start the necessary systemd units
|
||||||
|
sudo systemctl enable php-fpm arsse
|
||||||
|
sudo systemctl restart php-fpm arsse
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that the above is the most concise process, not necessarily the recommended one. In particular [it is recommended](https://wiki.archlinux.org/title/PHP#Extensions) to use `/etc/php/conf.d/` to enable PHP extensions rather than editing `php.ini` as done above.
|
||||||
|
|
||||||
|
# Web server configuration
|
||||||
|
|
||||||
|
Sample configuration for both Nginx and Apache HTTP Server can be found in `/etc/webapps/arsse/nginx/` and `/etc/webapps/arsse/apache/`, respectively. The `example.conf` files are basic virtual host examples; the other files they include should normally be usable without modification, but may be modified if desired.
|
||||||
|
|
||||||
|
If using Apache HTTP Server the `mod_proxy` and `mod_proxy_fcgi` modules must be enabled. This can be achieved by adding the following lines to your virtual host or global configuration:
|
||||||
|
|
||||||
|
```apache
|
||||||
|
LoadModule proxy_module modules/mod_proxy.so
|
||||||
|
LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so
|
||||||
|
```
|
||||||
|
|
||||||
|
No additional set-up is required for Nginx.
|
||||||
|
|
||||||
|
# Next steps
|
||||||
|
|
||||||
|
If using a database other than SQLite, you will likely want to [set it up](/en/Getting_Started/Database_Setup) before doing anything else.
|
||||||
|
|
||||||
|
In order for The Arsse to serve users, those users [must be created](/en/Using_The_Arsse/Managing_Users).
|
||||||
|
|
||||||
|
You may also want to review the `config.defaults.php` file included in `/etc/webapps/arsse/` or consult [the documentation for the configuration file](/en/Getting_Started/Configuration), though The Arsse should function with the default configuration.
|
||||||
|
|
||||||
|
# Upgrading
|
||||||
|
|
||||||
|
Upgrading The Arsse is done like any other package. By default The Arsse will perform any required database schema upgrades when the new version is executed, so the service does need to be restarted:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo systemctl restart arsse
|
||||||
|
```
|
||||||
|
|
||||||
|
Occasionally changes to Web server configuration have been required, such as when new protocols become supported; these changes are always explicit in the `UPGRADING` file.
|
|
@ -0,0 +1,72 @@
|
||||||
|
[TOC]
|
||||||
|
|
||||||
|
# Downloading The Arsse
|
||||||
|
|
||||||
|
Since version 0.10.0 pre-built Debian packages for The Arsse are available from the [OpenSUSE Build Service](https://build.opensuse.org/) (OBS) under [the author's personal project repository](https://build.opensuse.org/package/show/home:JKingWeb/arsse). This is the preferred method for instaling the software and is the means documented below.
|
||||||
|
|
||||||
|
Generic release tarballs may also be downloaded [from our Web site](https://thearsse.com), and a Debian package built manually. Installing directly from the generic release tarball without producing a Debian package is not recommended as the Debian packages make the set-up process on Debian systems significantly simpler.
|
||||||
|
|
||||||
|
# Adding the repository
|
||||||
|
|
||||||
|
In order to install The Arsse, the OBS repository must first be configured along with its signing key:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Add the key
|
||||||
|
wget -q -O - "https://download.opensuse.org/repositories/home:/JKingWeb/Debian_Unstable/Release.key" | gpg --dearmor | sudo tee "/usr/share/keyrings/arsse-obs-keyring.gpg" >/dev/null
|
||||||
|
# Add the repository
|
||||||
|
echo "deb [signed-by=/usr/share/keyrings/arsse-obs-keyring.gpg] https://download.opensuse.org/repositories/home:/JKingWeb/Debian_Unstable/ ./" | sudo tee "/etc/apt/sources.list.d/arsse-obs.list" >/dev/null
|
||||||
|
# Update APT's database
|
||||||
|
sudo apt update -qq
|
||||||
|
```
|
||||||
|
|
||||||
|
Please note that the "Unstable" qualifier in the repository URL is a reference to Debian's "sid" release and is not a reflection on The Arsse's stability. The repository should be suitable for any Debian version or derivative which includes a sufficiently recent version of PHP.
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
|
||||||
|
Once the OBS repository is configured, installing The Arsse is achieved with a single command:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo apt install arsse
|
||||||
|
```
|
||||||
|
|
||||||
|
After installation is complete The Arsse will be started automatically.
|
||||||
|
|
||||||
|
During the installation process you will be prompted whether to allow `dbconfig-common` to configure The Arsse's database automatically. The default `sqlite3` (SQLite) option is a good choice, but `pgsql` (PostgreSQL) and `mysql` (MySQL) are possible alternatives. If you wish to [use a database other than SQLite](/en/Getting_Started/Database_Setup/index), you should install it before installing The Arsse:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Install PostgreSQL
|
||||||
|
sudo apt install postgresql php-pgsql
|
||||||
|
# Install MySQL
|
||||||
|
sudo apt install mysql-server php-mysql
|
||||||
|
# Install SQLite explicitly
|
||||||
|
sudo apt install php-sqlite3
|
||||||
|
```
|
||||||
|
|
||||||
|
If you wish to change the database backend after having installed The Arsse, running `dpkg-reconfigure` after installing the database server can be used to achieve this:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo dpkg-reconfigure arsse
|
||||||
|
```
|
||||||
|
|
||||||
|
# Web server configuration
|
||||||
|
|
||||||
|
Sample configuration for both Nginx and Apache HTTP Server can be found in `/etc/arsse/nginx/` and `/etc/arsse/apache/`, respectively. The `example.conf` files are basic virtual host examples; the other files they include should normally be usable without modification, but may be modified if needed or desired. In particularly users of systems older than Debian 11 (Bullseye) or Ubuntu 20.04 (Focal Fossa) or derivatives will need to change the PHP-FPM socket path in Nginx or Apache's `arsse.conf`.
|
||||||
|
|
||||||
|
In order to use Apache HTTP Server the FastCGI proxy module must be enabled and the server restarted:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo a2enmod proxy proxy_fcgi
|
||||||
|
sudo systemctl restart apache2
|
||||||
|
```
|
||||||
|
|
||||||
|
No additional set-up is required for Nginx.
|
||||||
|
|
||||||
|
# Next steps
|
||||||
|
|
||||||
|
In order for The Arsse to serve users, those users [must be created](/en/Using_The_Arsse/Managing_Users).
|
||||||
|
|
||||||
|
You may also want to review the `config.defaults.php` file included in the download package and create [a configuration file](/en/Getting_Started/Configuration), though The Arsse can function even without using a configuration file.
|
||||||
|
|
||||||
|
# Upgrading
|
||||||
|
|
||||||
|
Upgrading The Arsse is done like any other package. Occasionally changes to Web server configuration have been required, such as when new protocols become supported; these changes are always explicit in the `UPGRADING` file.
|
|
@ -0,0 +1,53 @@
|
||||||
|
# Downloading The Arsse
|
||||||
|
|
||||||
|
The Arsse should run on any operating system for which PHP and a Web server are available, but only the combination of Linux, Systemd, Nginx, and PHP-FPM has been extensively tested.
|
||||||
|
|
||||||
|
Below are very generic instructions and suggestions for installing The Arsse on systems for which pre-built packages are not available.
|
||||||
|
|
||||||
|
# Requirements
|
||||||
|
|
||||||
|
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:
|
||||||
|
- [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:
|
||||||
|
- [sqlite3](https://php.net/manual/en/book.sqlite3.php) or [pdo_sqlite](https://php.net/manual/en/ref.pdo-sqlite.php) for SQLite databases
|
||||||
|
- [pgsql](https://php.net/manual/en/book.pgsql.php) or [pdo_pgsql](https://php.net/manual/en/ref.pdo-pgsql.php) for PostgreSQL 10 or later databases
|
||||||
|
- [mysqli](https://php.net/manual/en/book.mysqli.php) or [pdo_mysql](https://php.net/manual/en/ref.pdo-mysql.php) for MySQL/Percona 8.0.11 or later databases
|
||||||
|
- [curl](https://php.net/manual/en/book.curl.php) (optional)
|
||||||
|
- [posix](https://php.net/manual/en/book.posix.php) and [pcntl](https://php.net/manual/en/book.pcntl.php) (both optional)
|
||||||
|
- An interface between PHP and the Web server, such as [PHP-FPM](https://php.net/manual/en/install.fpm.php)
|
||||||
|
- Privileges either to create and run system services, or to run cron jobs
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
|
||||||
|
1. Download [the latest release](https://thearsse.com/releases/current) and extract it somewhere, such as `/usr/share/arsse/`
|
||||||
|
2. [Set up your database](/en/Getting_Started/Database_Setup)
|
||||||
|
3. Create [a configuration file](/en/Getting_Started/Configuration) if needed
|
||||||
|
4. Consult the files under `dist/nginx` and `dist/apache` for sample Web server configuration
|
||||||
|
5. Consult `dist/arsse` for a sample executable script which drops privileges on POSIX systems
|
||||||
|
6. Start the newsfeed fetching service:
|
||||||
|
- Sample Systemd service files are available under `dist/systemd`
|
||||||
|
- A sample System V init script is available in `dist/init.sh`
|
||||||
|
- A persistent process can be started by running `php arsse.php daemon`
|
||||||
|
- It is also possible [to use cron](/en/Using_The_Arsse/Other_Topics.html#page_Refreshing_newsfeeds_with_a_cron_job) or a similar task-scheduling tool
|
||||||
|
7. [Create users](/en/Using_The_Arsse/Managing_Users) to grant them access
|
||||||
|
|
||||||
|
# Upgrading
|
||||||
|
|
||||||
|
Upgrading The Arsse is usually simple:
|
||||||
|
|
||||||
|
1. Download the latest release
|
||||||
|
2. Check the `UPGRADING` file for any special notes
|
||||||
|
3. Stop the newsfeed refreshing service if it is running
|
||||||
|
4. Back up your configurationm and database
|
||||||
|
5. Extract the new version on top of the old one
|
||||||
|
6. Restart the newsfeed refreshing service
|
||||||
|
|
||||||
|
By default The Arsse will perform any required database schema upgrades when the new version is executed.
|
||||||
|
|
||||||
|
Occasionally changes to Web server configuration have been required, when new protocols become supported; such changes are always explicit in the `UPGRADING` file
|
|
@ -0,0 +1,16 @@
|
||||||
|
# Installing from a package manager
|
||||||
|
|
||||||
|
We currently provide a few pre-built installation packages for the following operating systems:
|
||||||
|
|
||||||
|
- [Arch Linux and derivatives](On_Arch_Linux)
|
||||||
|
- [Debian and derivatives](On_Debian_and_Derivatives)
|
||||||
|
|
||||||
|
These packages significantly simplify installation, though a bit of manual effort may still be required. Updating The Arsse using these packages should require no manual intervention.
|
||||||
|
|
||||||
|
# Installing manually
|
||||||
|
|
||||||
|
For other systems The Arsse must currently be installed manually. As each operating system is different, we can only provide very general instructions:
|
||||||
|
|
||||||
|
- [Installing on other systems](On_Other_Systems)
|
||||||
|
|
||||||
|
We hope to support more operating systems in the future.
|
|
@ -1,107 +0,0 @@
|
||||||
[TOC]
|
|
||||||
|
|
||||||
# Preface
|
|
||||||
|
|
||||||
As a PHP application, The Arsse requires the aid of a Web server in order to communicate with clients. How to install and configure a Web server in general is outside the scope of this document, but this section provides examples and advice for Web server configuration specific to The Arsse. Any server capable of interfacing with PHP should work, though we have only tested Nginx and Apache 2.4.
|
|
||||||
|
|
||||||
Samples included here only cover the bare minimum for configuring a virtual host. In particular, configuration for HTTPS (which is highly recommended) is omitted for the sake of clarity
|
|
||||||
|
|
||||||
# Configuration for Nginx
|
|
||||||
|
|
||||||
```nginx
|
|
||||||
server {
|
|
||||||
server_name example.com;
|
|
||||||
listen 80; # adding HTTPS configuration is highly recommended
|
|
||||||
root /usr/share/arsse/www; # adjust according to your installation path
|
|
||||||
|
|
||||||
location / {
|
|
||||||
try_files $uri $uri/ =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
location @arsse {
|
|
||||||
fastcgi_pass unix:/var/run/php/php7.2-fpm.sock; # adjust according to your system configuration
|
|
||||||
fastcgi_pass_header Authorization; # required if the Arsse is to perform its own HTTP authentication
|
|
||||||
fastcgi_pass_request_body on;
|
|
||||||
fastcgi_pass_request_headers on;
|
|
||||||
fastcgi_intercept_errors off;
|
|
||||||
fastcgi_buffering off;
|
|
||||||
fastcgi_param SCRIPT_FILENAME /usr/share/arsse/arsse.php; # adjust according to your installation path
|
|
||||||
fastcgi_param REQUEST_METHOD $request_method;
|
|
||||||
fastcgi_param CONTENT_TYPE $content_type;
|
|
||||||
fastcgi_param CONTENT_LENGTH $content_length;
|
|
||||||
fastcgi_param REQUEST_URI $uri;
|
|
||||||
fastcgi_param QUERY_STRING $query_string;
|
|
||||||
fastcgi_param HTTPS $https if_not_empty;
|
|
||||||
fastcgi_param REMOTE_USER $remote_user;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Nextcloud News protocol
|
|
||||||
location /index.php/apps/news/api {
|
|
||||||
try_files $uri @arsse;
|
|
||||||
|
|
||||||
location ~ ^/index\.php/apps/news/api/?$ {
|
|
||||||
# this path should not be behind HTTP authentication
|
|
||||||
try_files $uri @arsse;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Tiny Tiny RSS protocol
|
|
||||||
location /tt-rss/api {
|
|
||||||
try_files $uri @arsse;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Tiny Tiny RSS feed icons
|
|
||||||
location /tt-rss/feed-icons/ {
|
|
||||||
try_files $uri @arsse;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Tiny Tiny RSS special-feed icons; these are static files
|
|
||||||
location /tt-rss/images/ {
|
|
||||||
# this path should not be behind HTTP authentication
|
|
||||||
try_files $uri =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Fever protocol
|
|
||||||
location /fever/ {
|
|
||||||
# this path should not be behind HTTP authentication
|
|
||||||
try_files $uri @arsse;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
# Configuration for Apache2
|
|
||||||
|
|
||||||
There are many ways for Apache to interface with PHP, but the recommended way today is to use `mod_proxy` and `mod_proxy_fcgi` to communicate with PHP-FPM. If necessary you can enable these modules on Debian systems using the following commands:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
sudo a2enmod proxy proxy_fcgi
|
|
||||||
sudo systemctl restart apache2
|
|
||||||
```
|
|
||||||
|
|
||||||
Afterward the follow virtual host configuration should work, after modifying path as appropriate:
|
|
||||||
|
|
||||||
```apache
|
|
||||||
# N.B. the unix:/var/run/php/php7.2-fpm.sock path used repeatedly below will
|
|
||||||
# vary from system to system and will be probably need to be changed
|
|
||||||
|
|
||||||
<VirtualHost *:80>
|
|
||||||
ServerName localhost
|
|
||||||
# adjust according to your installation path
|
|
||||||
DocumentRoot /usr/share/arsse/www
|
|
||||||
|
|
||||||
# adjust according to your installation path
|
|
||||||
ProxyFCGISetEnvIf "true" SCRIPT_FILENAME "/usr/share/arsse/arsse.php"
|
|
||||||
ProxyPreserveHost On
|
|
||||||
|
|
||||||
# Nextcloud News v1.2, Tiny Tiny RSS API, TT-RSS newsfeed icons
|
|
||||||
<LocationMatch "(/index\.php/apps/news/api/?.+|/tt-rss/(api|feed-icons))">
|
|
||||||
ProxyPass "unix:/var/run/php/php7.2-fpm.sock|fcgi://localhost/usr/share/arsse"
|
|
||||||
</LocationMatch>
|
|
||||||
|
|
||||||
# Nextcloud News API detection, Fever API
|
|
||||||
<LocationMatch "(/index\.php/apps/news/api/?$|/fever)">
|
|
||||||
# these locations should not be behind HTTP authentication
|
|
||||||
ProxyPass "unix:/var/run/php/php7.2-fpm.sock|fcgi://localhost/usr/share/arsse"
|
|
||||||
</LocationMatch>
|
|
||||||
</VirtualHost>
|
|
||||||
```
|
|
|
@ -8,9 +8,17 @@
|
||||||
<dt>Minimum version</dt>
|
<dt>Minimum version</dt>
|
||||||
<dd>3.8.3</dd>
|
<dd>3.8.3</dd>
|
||||||
<dt>Configuration</dt>
|
<dt>Configuration</dt>
|
||||||
<dd><a href="../Configuration.html#page_Database-settings">General</a>, <a href="../Configuration.html#page_Database-settings-specific-to-SQLite-3">Specific</a></dd>
|
<dd><a href="../Configuration.html#page_Database_settings">General</a>, <a href="../Configuration.html#page_Database_settings_specific_to_SQLite_3">Specific</a></dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
SQLite requires very little set-up. By default the database will be created at the root of The Arsse's program directory (e.g. `/usr/share/arsse/arsse.db`), but this can be changed with the [`dbSQLite3File` setting](/en/Getting_Started/Configuration#page_dbSQLite3File).
|
SQLite requires very little set-up. By default the database will be created at one of the following locations depending on installation method:
|
||||||
|
|
||||||
Regardless of the location chosen, The Arsse **must** be able to both read from and write to the database file, as well as create files in its directory. This is because SQLite also creates a write-ahead log file and a shared-memory file during operation.
|
| Installation method | Default database path |
|
||||||
|
|---------------------|------------------------------------------------------|
|
||||||
|
| Arch Linux package | `/var/lib/arsse/arsse.db` |
|
||||||
|
| Debian package | `/var/lib/dbconfig-common/sqlite3/arsse/arsse` |
|
||||||
|
| Manual installation | `arsse.db` in the The Arsse's installation directory |
|
||||||
|
|
||||||
|
This path can be changed with the [`dbSQLite3File` setting](/en/Getting_Started/Configuration#page_dbSQLite3File).
|
||||||
|
|
||||||
|
Regardless of the location used, The Arsse **must** be able to both read from and write to the database file, as well as create files in its directory. This is because SQLite also creates a write-ahead log file and a shared-memory file during operation.
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<dt>Minimum version</dt>
|
<dt>Minimum version</dt>
|
||||||
<dd>10</dd>
|
<dd>10</dd>
|
||||||
<dt>Configuration</dt>
|
<dt>Configuration</dt>
|
||||||
<dd><a href="../Configuration.html#page_Database-settings">General</a>, <a href="../Configuration.html#page_Database-settings-specific-to-PostgreSQL">Specific</a></dd>
|
<dd><a href="../Configuration.html#page_Database_settings">General</a>, <a href="../Configuration.html#page_Database_settings_specific_to_PostgreSQL">Specific</a></dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
If for whatever reason an SQLite database does not suit your configuration, PostgreSQL is the best alternative. It is functionally equivalent to SQLite in every way.
|
If for whatever reason an SQLite database does not suit your configuration, PostgreSQL is the best alternative. It is functionally equivalent to SQLite in every way.
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<dt>Minimum version</dt>
|
<dt>Minimum version</dt>
|
||||||
<dd>8.0.11</dd>
|
<dd>8.0.11</dd>
|
||||||
<dt>Configuration</dt>
|
<dt>Configuration</dt>
|
||||||
<dd><a href="../Configuration.html#page_Database-settings">General</a>, <a href="../Configuration.html#page_Database-settings-specific-to-MySQL">Specific</a></dd>
|
<dd><a href="../Configuration.html#page_Database_settings">General</a>, <a href="../Configuration.html#page_Database_settings_specific_to_MySQL">Specific</a></dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
While MySQL can be used as a database for The Arsse, this is **not recommended** due to MySQL's technical limitations. It is fully functional, but may fail with some newsfeeds where other database systems do not. Additionally, it is particularly important before upgrading from one version of The Arsse to the next to back up your database: a failure in a database upgrade can corrupt your database much more easily than when using other database systems.
|
While MySQL can be used as a database for The Arsse, this is **not recommended** due to MySQL's technical limitations. It is fully functional, but may fail with some newsfeeds where other database systems do not. Additionally, it is particularly important before upgrading from one version of The Arsse to the next to back up your database: a failure in a database upgrade can corrupt your database much more easily than when using other database systems.
|
||||||
|
|
|
@ -1,6 +1,14 @@
|
||||||
# The configuration file
|
# The configuration file
|
||||||
|
|
||||||
The Arsse looks for configuration in a file named `config.php` in the directory where it is installed. For example, if The Arsse is installed at `/usr/share/arsse`, it will look for configuration in the file `/usr/share/arsse/config.php`. It is not an error for this file not to exist or to be empty: The Arsse will function with no configuration whatsoever, provided other conditions allow.
|
Depending on how The Arsse was installed, it will look for configuration in the following places:
|
||||||
|
|
||||||
|
| Installation method | Default configuration file path |
|
||||||
|
|---------------------|--------------------------------------------------------|
|
||||||
|
| Arch Linux package | `/etc/webapps/arsse/config.php` |
|
||||||
|
| Debian package | `/etc/arsse/config.php` |
|
||||||
|
| Manual installation | `config.php` in the The Arsse's installation directory |
|
||||||
|
|
||||||
|
It is not an error for this file not to exist or to be empty: The Arsse will function with no configuration whatsoever, provided other conditions allow.
|
||||||
|
|
||||||
The configuration file is a PHP script which returns an associative array with keys and values for one or more settings. Any settings which are not specified in the configuration file will be set to their defaults. Invalid values will cause an error on start-up, while unknown keys are ignored. A basic configuration file might look like this:
|
The configuration file is a PHP script which returns an associative array with keys and values for one or more settings. Any settings which are not specified in the configuration file will be set to their defaults. Invalid values will cause an error on start-up, while unknown keys are ignored. A basic configuration file might look like this:
|
||||||
|
|
||||||
|
@ -12,10 +20,10 @@ The configuration file is a PHP script which returns an associative array with k
|
||||||
];
|
];
|
||||||
```
|
```
|
||||||
|
|
||||||
The `config.defaults.php` file included with copies of The Arsse contains an annotated listing of every configuration setting with its default value. The settings are also documented in more detail below.
|
|
||||||
|
|
||||||
# List of all settings
|
# List of all settings
|
||||||
|
|
||||||
|
The `config.defaults.php` file included with copies of The Arsse contains an annotated listing of every configuration setting with its default value. The settings are also documented in more detail below.
|
||||||
|
|
||||||
## General settings
|
## General settings
|
||||||
|
|
||||||
### lang
|
### lang
|
||||||
|
@ -321,7 +329,7 @@ It is also possible to specify the fully-qualified name of a class which impleme
|
||||||
|
|
||||||
The interval the newsfeed fetching service observes between checks for new articles. Note that requests to foreign servers are not necessarily made at this frequency: each newsfeed is assigned its own time at which to be next retrieved. This setting instead defines the length of time the fetching service will sleep between periods of activity.
|
The interval the newsfeed fetching service observes between checks for new articles. Note that requests to foreign servers are not necessarily made at this frequency: each newsfeed is assigned its own time at which to be next retrieved. This setting instead defines the length of time the fetching service will sleep between periods of activity.
|
||||||
|
|
||||||
Consult "[How Often Newsfeeds Are Fetched](/en/Using_The_Arsse/Keeping_Newsfeeds_Up_to_Date#page_Appendix-how-often-newsfeeds-are-fetched)" for details on how often newsfeeds are fetched.
|
Consult "[How Often Newsfeeds Are Fetched](/en/Using_The_Arsse/Other_Topics#page_How_often_newsfeeds_are_fetched)" for details on newsfeed update frequency.
|
||||||
|
|
||||||
### serviceQueueWidth
|
### serviceQueueWidth
|
||||||
|
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
Presently installing and setting up The Arsse is a manual process. We hope to have pre-configured installation packages available for various operating systems eventually, but for now the pages in this section should help get you up and running.
|
|
||||||
|
|
||||||
Though The Arsse itself makes no assumptions about the operating system which hosts it, we use and have the most experience with Debian; the instructions contained here therefore are for Debian systems and will probably either not work with other systems or not be consistent with their conventions. Nevertheless, they should still serve as a useful guide.
|
|
|
@ -9,28 +9,28 @@ This section describes in brief some CLI commands. Please read [the general note
|
||||||
When first installed, The Arsse has no users configured. You may add users by executing the following command:
|
When first installed, The Arsse has no users configured. You may add users by executing the following command:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo -u www-data php arsse.php user add "user@example.com" "example password"
|
sudo arsse user add "user@example.com" "example password"
|
||||||
```
|
```
|
||||||
|
|
||||||
The password argument is optional: if no password is provided, a random one is generated and printed out:
|
The password argument is optional: if no password is provided, a random one is generated and printed out:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ sudo -u www-data php arsse.php user add "jane.doe"
|
$ sudo arsse user add "jane.doe"
|
||||||
Ji0ivMYqi6gKxQK1MHuE
|
Ji0ivMYqi6gKxQK1MHuE
|
||||||
```
|
```
|
||||||
|
|
||||||
# Setting and changing passwords
|
# Setting and changing passwords
|
||||||
|
|
||||||
Setting's a user's password is practically identical to adding a password:
|
Setting a user's password is nearly identical to adding a user:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo -u www-data php arsse.php user set-pass "user@example.com" "new password"
|
sudo arsse user set-pass "user@example.com" "new password"
|
||||||
```
|
```
|
||||||
|
|
||||||
As when adding a user, the password argument is optional: if no password is provided, a random one is generated and printed out:
|
As when adding a user, the password argument is optional: if no password is provided, a random one is generated and printed out:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ sudo -u www-data php arsse.php user set-pass "jane.doe"
|
$ sudo arsse user set-pass "jane.doe"
|
||||||
Ummn173XjbJT4J3Gnx0a
|
Ummn173XjbJT4J3Gnx0a
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -39,17 +39,50 @@ Ummn173XjbJT4J3Gnx0a
|
||||||
Before a user can make use of [the Fever protocol](/en/Supported_Protocols/Fever), a Fever-specific password for that user must be set. It is _highly recommended_ that this not be the samer as the user's main password. The password can be set by adding the `--fever` option to the normal password-changing command:
|
Before a user can make use of [the Fever protocol](/en/Supported_Protocols/Fever), a Fever-specific password for that user must be set. It is _highly recommended_ that this not be the samer as the user's main password. The password can be set by adding the `--fever` option to the normal password-changing command:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo -u www-data php arsse.php user set-pass --fever "user@example.com" "fever password"
|
sudo arsse user set-pass --fever "user@example.com" "fever password"
|
||||||
```
|
```
|
||||||
|
|
||||||
As when setting a main password, the password argument is optional: if no password is provided, a random one is generated and printed out:
|
As when setting a main password, the password argument is optional: if no password is provided, a random one is generated and printed out:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ sudo -u www-data php arsse.php user set-pass --fever "jane.doe"
|
$ sudo arsse user set-pass --fever "jane.doe"
|
||||||
YfZJHq4fNTRUKDYhzQdR
|
YfZJHq4fNTRUKDYhzQdR
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Managing login tokens for Miniflux
|
||||||
|
|
||||||
|
[Miniflux](/en/Supported_Protocols/Miniflux) clients may optionally log in using tokens: randomly-generated strings which act as persistent passwords. For now these must be generated using the command-line interface:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ sudo arsse token create "jane.doe"
|
||||||
|
xRK0huUE9KHNHf_x_H8JG0oRDo4t_WV44whBtr8Ckf0=
|
||||||
|
```
|
||||||
|
|
||||||
|
Multiple tokens may be generated for use with different clients, and descriptive labels can be assigned for later identification:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ sudo arsse token create "jane.doe" Newsflash
|
||||||
|
xRK0huUE9KHNHf_x_H8JG0oRDo4t_WV44whBtr8Ckf0=
|
||||||
|
$ sudo arsse token create "jane.doe" Reminiflux
|
||||||
|
L7asI2X_d-krinGJd1GsiRdFm2o06ZUlgD22H913hK4=
|
||||||
|
```
|
||||||
|
|
||||||
|
There are also commands for listing and revoking tokens. Please consult the integrated help for more details.
|
||||||
|
|
||||||
|
# Setting and changing user metadata
|
||||||
|
|
||||||
|
Users may also have various metadata properties set. These largely exist for compatibility with [the Miniflux protocol](/en/Supported_Protocols/Miniflux) and have no significant effect. One exception to this, however, is the `admin` flag, which signals whether the user may perform privileged operations where they exist in the supported protocols.
|
||||||
|
|
||||||
|
The flag may be changed using the following command:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo arsse user set "jane.doe" admin true
|
||||||
|
```
|
||||||
|
|
||||||
|
As a shortcut it is also possible to create administrators directly:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo arsse user add "user@example.com" "example password" --admin
|
||||||
|
```
|
||||||
|
|
||||||
|
Please consult the integrated help for more details on metadata and their effects.
|
||||||
|
|
|
@ -9,7 +9,7 @@ This section describes in brief some CLI commands. Please read [the general note
|
||||||
It's possible to import not only newsfeeds but also folders and Fever groups using OPML files. The process is simple:
|
It's possible to import not only newsfeeds but also folders and Fever groups using OPML files. The process is simple:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo -u www-data php arsse.php import "user@example.com" "subscriptions.opml"
|
sudo arsse import "user@example.com" "subscriptions.opml"
|
||||||
```
|
```
|
||||||
|
|
||||||
The importer is forgiving, but some OPML files may fail, with the reason printed out. Files are either imported in total, or not at all.
|
The importer is forgiving, but some OPML files may fail, with the reason printed out. Files are either imported in total, or not at all.
|
||||||
|
@ -19,7 +19,7 @@ The importer is forgiving, but some OPML files may fail, with the reason printed
|
||||||
It's possible to export not only newsfeeds but also folders and Fever groups to OPML files. The process is simple:
|
It's possible to export not only newsfeeds but also folders and Fever groups to OPML files. The process is simple:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo -u www-data php arsse.php export "user@example.com" "subscriptions.opml"
|
sudo arsse export "user@example.com" "subscriptions.opml"
|
||||||
```
|
```
|
||||||
|
|
||||||
The output might look like this:
|
The output might look like this:
|
||||||
|
@ -46,9 +46,9 @@ Not all protocols supported by The Arsse allow modifying newsfeeds or folders, e
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# export your newsfeeds
|
# export your newsfeeds
|
||||||
sudo -u www-data php arsse.php export "user@example.com" "subscriptions.opml"
|
sudo arsse export "user@example.com" "subscriptions.opml"
|
||||||
# make any changes you want in your editor of choice
|
# make any changes you want in your editor of choice
|
||||||
nano "subscriptions.opml"
|
nano "subscriptions.opml"
|
||||||
# re-import the modified information
|
# re-import the modified information
|
||||||
sudo -u www-data php arsse.php import "user@example.com" "subscriptions.opml" --replace
|
sudo arsse import "user@example.com" "subscriptions.opml" --replace
|
||||||
```
|
```
|
||||||
|
|
|
@ -1,49 +0,0 @@
|
||||||
[TOC]
|
|
||||||
|
|
||||||
# Preface
|
|
||||||
|
|
||||||
In normal operation The Arsse is expected to regularly check whether newsfeeds might have new articles, then fetch them and process them to present new or updated articles to clients. This can be achieved either by having The Arsse operate a persistent background process (termed a [daemon](https://en.wikipedia.org/wiki/Daemon_(computing)) or service), or by using an external scheduler to periodically perform single checks. Normally a daemon is preferred.
|
|
||||||
|
|
||||||
There are many ways to administer daemons, and many schedulers can be used. This section outlines a few, but many other arrangements are possible.
|
|
||||||
|
|
||||||
# As a daemon via systemd
|
|
||||||
|
|
||||||
The Arsse includes a sample systemd service unit file which can be used to quickly get a daemon running with the following procedure:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Copy the service unit
|
|
||||||
sudo cp "/usr/share/arsse/dist/arsse.service" "/etc/systemd/system"
|
|
||||||
# Modify the unit file if needed
|
|
||||||
sudoedit "/etc/systemd/system/arsse.service"
|
|
||||||
# Enable and start the service
|
|
||||||
sudo systemctl enable --now arsse
|
|
||||||
```
|
|
||||||
|
|
||||||
The Arsse's feed updater can then be manipulated as with any other service. Consult [the `systemctl` manual](https://www.freedesktop.org/software/systemd/man/systemctl.html) for details.
|
|
||||||
|
|
||||||
# As a cron job
|
|
||||||
|
|
||||||
Keeping newsfeeds updated with [cron](https://en.wikipedia.org/wiki/Cron) is not difficult. Simply run the following command:
|
|
||||||
|
|
||||||
|
|
||||||
```sh
|
|
||||||
sudo crontab -u www-data -e
|
|
||||||
```
|
|
||||||
|
|
||||||
And add a line such as this one:
|
|
||||||
|
|
||||||
```
|
|
||||||
*/2 * * * * /usr/bin/env php /usr/share/arsse/arsse.php refresh-all
|
|
||||||
```
|
|
||||||
|
|
||||||
Thereafter The Arsse's will be scheduled to check newsfeeds every two minutes. Consult the manual pages for the `crontab` [format](http://man7.org/linux/man-pages/man5/crontab.5.html) and [command](http://man7.org/linux/man-pages/man1/crontab.1.html) for details.
|
|
||||||
|
|
||||||
# Appendix: how often newsfeeds are fetched
|
|
||||||
|
|
||||||
Though by default The Arsse will wake up every two minutes, newsfeeds are not actually downloaded so frequently. Instead, each newsfeed is assigned a time at which it should next be fetched, and once that time is reached a [conditional request](https://developer.mozilla.org/en-US/docs/Web/HTTP/Conditional_requests) is made. The interval between requests for a particular newsfeed can vary from 15 minutes to 24 hours based on multiple factors such as:
|
|
||||||
|
|
||||||
- The length of time since the newsfeed last changed
|
|
||||||
- The interval between publishing of articles in the newsfeed
|
|
||||||
- Whether the last fetch or last several fetches resulted in error
|
|
||||||
|
|
||||||
As a general rule, newsfeeds which change frequently are checked frequently, and those which change seldom are fetched at most daily.
|
|
34
docs/en/025_Using_The_Arsse/030_Other_Topics.md
Normal file
34
docs/en/025_Using_The_Arsse/030_Other_Topics.md
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
[TOC]
|
||||||
|
|
||||||
|
# Preface
|
||||||
|
|
||||||
|
This section describes in brief some CLI commands. Please read [the general notes on the command line interface](index) before continuing.
|
||||||
|
|
||||||
|
# Refreshing newsfeeds with a cron job
|
||||||
|
|
||||||
|
Normally The Arsse has a systemd service which checks newsfeeds for updates and processes them into its database for the user. If for whatever reason this is not practical a [cron](https://en.wikipedia.org/wiki/Cron) job may be used instead.
|
||||||
|
|
||||||
|
Keeping newsfeeds updated with cron is not difficult. Simply run the following command:
|
||||||
|
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo crontab -u arsse -e
|
||||||
|
```
|
||||||
|
|
||||||
|
And add a line such as this one:
|
||||||
|
|
||||||
|
```
|
||||||
|
*/2 * * * * /usr/bin/arsse refresh-all
|
||||||
|
```
|
||||||
|
|
||||||
|
Thereafter The Arsse's will be scheduled to check newsfeeds every two minutes. Consult the manual pages for the `crontab` [format](http://man7.org/linux/man-pages/man5/crontab.5.html) and [command](http://man7.org/linux/man-pages/man1/crontab.1.html) for details.
|
||||||
|
|
||||||
|
# How often newsfeeds are fetched
|
||||||
|
|
||||||
|
Though by default The Arsse will wake up every two minutes, newsfeeds are not actually downloaded so frequently. Instead, each newsfeed is assigned a time at which it should next be fetched, and once that time is reached a [conditional request](https://developer.mozilla.org/en-US/docs/Web/HTTP/Conditional_requests) is made. The interval between requests for a particular newsfeed can vary from 15 minutes to 24 hours based on multiple factors such as:
|
||||||
|
|
||||||
|
- The length of time since the newsfeed last changed
|
||||||
|
- The interval between publishing of articles in the newsfeed
|
||||||
|
- Whether the last fetch or last several fetches resulted in error
|
||||||
|
|
||||||
|
As a general rule, newsfeeds which change frequently are checked frequently, and those which change seldom are fetched at most daily.
|
|
@ -1,12 +0,0 @@
|
||||||
Upgrading The Arsse is usually simple:
|
|
||||||
|
|
||||||
1. Download the latest release
|
|
||||||
2. Check the `UPGRADING` file for any special notes
|
|
||||||
3. Stop the newsfeed refreshing service if it is running
|
|
||||||
4. Extract the new version on top of the old one
|
|
||||||
5. Ensure permissions are still correct
|
|
||||||
6. Restart the newsfeed refreshing service
|
|
||||||
|
|
||||||
By default The Arsse will perform any required database schema upgrades when the new version is executed, and release packages contain all newly required library dependencies.
|
|
||||||
|
|
||||||
Occasionally changes to Web server configuration have been required, when new protocols become supported; such changes are always explicit in the `UPGRADING` file
|
|
|
@ -2,19 +2,8 @@
|
||||||
|
|
||||||
This section details a few administrative tasks which may need to be performed after installing The Arsse. As no Web-based administrative interface is included, these tasks are generally performed via command line interface.
|
This section details a few administrative tasks which may need to be performed after installing The Arsse. As no Web-based administrative interface is included, these tasks are generally performed via command line interface.
|
||||||
|
|
||||||
Though this section describes some commands briefly, complete documentation of The Arsse's command line interface is not included in this manual. Documentation for CLI commands can instead be viewed with the CLI itself by executing `php arsse.php --help`.
|
Though this section describes some commands briefly, complete documentation of The Arsse's command line interface is not included in this manual. Documentation for CLI commands can instead be viewed using the system manual service by executing `man arsse`.
|
||||||
|
|
||||||
# A Note on Command Invocation
|
# A Note on Command Invocation
|
||||||
|
|
||||||
Particularly if using an SQLite database, it's important that administrative commands be executed as the same user who owns The Arsse's files. To that end the examples in this section all use the verbose formulation `sudo -u www-data php arsse.php` (with `www-data` being the user under which Web servers run in Debian), but it is possible to simplify invocation to `sudo arsse` if an executable file named `arsse` is created somewhere in the sudo path with the following content:
|
Particularly if using an SQLite database, it's important that administrative commands be executed as the same user who owns The Arsse's files. To that end our releases include an `arsse` executable which drops privileges when executed as root. Commands in this section assume this executable is being used.
|
||||||
|
|
||||||
```php
|
|
||||||
#! /usr/bin/env php
|
|
||||||
<?php
|
|
||||||
if (posix_geteuid() == 0) {
|
|
||||||
$info = posix_getpwnam("www-data");
|
|
||||||
posix_setegid($info['gid']);
|
|
||||||
posix_seteuid($info['uid']);
|
|
||||||
}
|
|
||||||
include "/usr/share/arsse/arsse.php";
|
|
||||||
```
|
|
||||||
|
|
60
docs/en/030_Supported_Protocols/005_Miniflux.md
Normal file
60
docs/en/030_Supported_Protocols/005_Miniflux.md
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
[TOC]
|
||||||
|
|
||||||
|
# About
|
||||||
|
|
||||||
|
<dl>
|
||||||
|
<dt>Supported since</dt>
|
||||||
|
<dd>0.9.0</dd>
|
||||||
|
<dt>Base URL</dt>
|
||||||
|
<dd>/</dd>
|
||||||
|
<dt>API endpoint</dt>
|
||||||
|
<dd>/v1/</dd>
|
||||||
|
<dt>Specifications</dt>
|
||||||
|
<dd><a href="https://miniflux.app/docs/api.html">API Reference</a>, <a href="https://miniflux.app/docs/rules.html#filtering-rules">Filtering Rules</a></dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
The Miniflux protocol is a fairly well-designed protocol supporting a wide variety of operations on newsfeeds, folders (termed "categories"), and articles; it also allows for user administration, and native OPML importing and exporting. Architecturally it is similar to the Nextcloud News protocol, but has more capabilities.
|
||||||
|
|
||||||
|
Miniflux version 2.0.28 is emulated, though not all features are implemented
|
||||||
|
|
||||||
|
# Missing features
|
||||||
|
|
||||||
|
- JSON Feed format is not suported
|
||||||
|
- Various feed-related features are not supported; attempting to use them has no effect
|
||||||
|
- Rewrite rules and scraper rules
|
||||||
|
- Custom User-Agent strings
|
||||||
|
- The `disabled`, `ignore_http_cache`, and `fetch_via_proxy` flags
|
||||||
|
- Changing the URL, username, or password of a feed
|
||||||
|
- Manually refreshing feeds
|
||||||
|
- Titles and types are not available during feed discovery and are filled with generic data
|
||||||
|
- Reading time is not calculated and will always be zero
|
||||||
|
- Only the first enclosure of an article is retained
|
||||||
|
- Comment URLs of articles are not exposed
|
||||||
|
|
||||||
|
# Differences
|
||||||
|
|
||||||
|
- Various error codes and messages differ due to significant implementation differences
|
||||||
|
- The "All" category is treated specially (see below for details)
|
||||||
|
- Feed and category titles consisting only of whitespace are rejected along with the empty string
|
||||||
|
- 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
|
||||||
|
|
||||||
|
# Behaviour of filtering (block and keep) rules
|
||||||
|
|
||||||
|
The Miniflux documentation gives only a brief example of a pattern for its filtering rules; the allowed syntax is described in full [in Google's documentation for RE2](https://github.com/google/re2/wiki/Syntax). Being a PHP application, The Arsse instead accepts [PCRE syntax](http://www.pcre.org/original/doc/html/pcresyntax.html) (or since PHP 7.3 [PCRE2 syntax](https://www.pcre.org/current/doc/html/pcre2syntax.html)), specifically in UTF-8 mode. Delimiters should not be included, and slashes should not be escaped; anchors may be used if desired. For example `^(?i)RE/MAX$` is a valid pattern.
|
||||||
|
|
||||||
|
For convenience the patterns are tested after collapsing whitespace. Unlike Miniflux, The Arsse tests the patterns against an article's author-supplied categories if they do not match its title. Also unlike Miniflux, when filter rules are modified they are re-evaluated against all applicable articles immediately.
|
||||||
|
|
||||||
|
# Special handling of the "All" category
|
||||||
|
|
||||||
|
Nextcloud News' root folder and Tiny Tiny RSS' "Uncategorized" catgory are mapped to Miniflux's initial "All" category. This Miniflux category can be renamed, but it cannot be deleted. Attempting to do so will delete the child feeds it contains, but not the category itself.
|
||||||
|
|
||||||
|
Because the root folder does not existing in the database as a separate entity, it will always sort first when ordering by `category_id` or `category_title`.
|
||||||
|
|
||||||
|
# Interaction with nested categories
|
||||||
|
|
||||||
|
Tiny Tiny RSS is unique in allowing newsfeeds to be grouped into categories nested to arbitrary depth. When newsfeeds are placed into nested categories, they simply appear in the top-level category when accessed via the Miniflux protocol. This does not affect OPML exports, where full nesting is preserved.
|
|
@ -24,7 +24,6 @@ It allows organizing newsfeeds into single-level folders, and supports a wide ra
|
||||||
- When marking articles as starred the feed ID is ignored, as they are not needed to establish uniqueness
|
- When marking articles as starred the feed ID is ignored, as they are not needed to establish uniqueness
|
||||||
- The feed updater ignores the `userId` parameter: feeds in The Arsse are deduplicated, and have no owner
|
- The feed updater ignores the `userId` parameter: feeds in The Arsse are deduplicated, and have no owner
|
||||||
- The `/feeds/all` route lists only feeds which should be checked for updates, and it also returns all `userId` attributes as empty strings: feeds in The Arsse are deduplicated, and have no owner
|
- The `/feeds/all` route lists only feeds which should be checked for updates, and it also returns all `userId` attributes as empty strings: feeds in The Arsse are deduplicated, and have no owner
|
||||||
- The API's "updater" routes do not require administrator priviledges as The Arsse has no concept of user classes
|
|
||||||
- The "updater" console commands mentioned in the protocol specification are not implemented, as The Arsse does not implement the required Nextcloud subsystems
|
- The "updater" console commands mentioned in the protocol specification are not implemented, as The Arsse does not implement the required Nextcloud subsystems
|
||||||
- The `lastLoginTimestamp` attribute of the user metadata is always the current time: The Arsse's implementation of the protocol is fully stateless
|
- The `lastLoginTimestamp` attribute of the user metadata is always the current time: The Arsse's implementation of the protocol is fully stateless
|
||||||
- Syntactically invalid JSON input will yield a `400 Bad Request` response instead of falling back to GET parameters
|
- Syntactically invalid JSON input will yield a `400 Bad Request` response instead of falling back to GET parameters
|
||||||
|
|
|
@ -23,7 +23,6 @@ The Fever protocol is incomplete, unusual, _and_ a product of proprietary softwa
|
||||||
|
|
||||||
- All feeds are considered "Kindling"
|
- All feeds are considered "Kindling"
|
||||||
- The "Hot Links" feature is not implemented; when requested, an empty array will be returned. As there is no way to classify a feed as a "Spark" in the protocol itself and no documentation exists on how link temperature was calculated, an implementation is unlikely to appear in the future
|
- The "Hot Links" feature is not implemented; when requested, an empty array will be returned. As there is no way to classify a feed as a "Spark" in the protocol itself and no documentation exists on how link temperature was calculated, an implementation is unlikely to appear in the future
|
||||||
- Favicons are not currently supported; all feeds have a simple blank image as their favicon unless the client finds the icons itself
|
|
||||||
|
|
||||||
# Special considerations
|
# Special considerations
|
||||||
|
|
||||||
|
@ -38,7 +37,7 @@ The Fever protocol is incomplete, unusual, _and_ a product of proprietary softwa
|
||||||
|
|
||||||
# Interaction with HTTP Authentication
|
# Interaction with HTTP Authentication
|
||||||
|
|
||||||
We are not aware of any Fever clients which respond to HTTP authentication challenges. If the Web server or The Arsse is configured to require successful HTTP authentication, Fever clients are not likely to be able to connect properly.
|
Fever was not designed with HTTP authentication in mind, and few clients respond to challenges. If the Web server or The Arsse is configured to require successful HTTP authentication, most Fever clients are not likely to be able to connect properly.
|
||||||
|
|
||||||
# Interaction with Folders
|
# Interaction with Folders
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
The Arsse was designed from the start as a server for multiple synchronization protocols which clients can make use of. Currently the following protocols are supported:
|
The Arsse was designed from the start as a server for multiple synchronization protocols which clients can make use of. Currently the following protocols are supported:
|
||||||
|
|
||||||
|
- [Miniflux](Miniflux)
|
||||||
- [Nextcloud News](Nextcloud_News)
|
- [Nextcloud News](Nextcloud_News)
|
||||||
- [Tiny Tiny RSS](Tiny_Tiny_RSS)
|
- [Tiny Tiny RSS](Tiny_Tiny_RSS)
|
||||||
- [Fever](Fever)
|
- [Fever](Fever)
|
||||||
|
|
|
@ -1,31 +1,89 @@
|
||||||
The Arsse does not at this time have any first party clients. However, because The Arsse [supports existing protocols](/en/Supported_Protocols), most clients built for these protocols are compatible with The Arsse. Below are those that we personally know of and have tested with The Arsse.
|
The Arsse does not at this time have any first party clients. However, because The Arsse [supports existing protocols](/en/Supported_Protocols), most clients built for these protocols are compatible with The Arsse. Below are those that we personally know of and have tested with The Arsse, presented in alphabetical order.
|
||||||
|
|
||||||
<table class="clients">
|
<table class="clients">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th rowspan="2">Name</th>
|
<th rowspan="2">Name</th>
|
||||||
<th rowspan="2">OS</th>
|
<th rowspan="2">OS</th>
|
||||||
<th colspan="3">Protocol</th>
|
<th colspan="4">Protocol</th>
|
||||||
<th rowspan="2">Notes</th>
|
<th rowspan="2">Notes</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th>Miniflux</th>
|
||||||
<th>Nextcloud News</th>
|
<th>Nextcloud News</th>
|
||||||
<th>Tiny Tiny RSS</th>
|
<th>Tiny Tiny RSS</th>
|
||||||
<th>Fever</th>
|
<th>Fever</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
<th colspan="7">Web</th>
|
||||||
<tr>
|
<tr>
|
||||||
<th colspan="6">Desktop</th>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><a href="https://github.com/yurikhan/miniflux-reader">Miniflux Reader</a></td>
|
||||||
|
<td></td>
|
||||||
|
<td class="Y">✔</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td>Functional, but has some display glitches.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><a href="https://github.com/jgerstbe/nx-news-ion">NX News</a></td>
|
||||||
|
<td></td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td class="Y">✔</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td>Extremely basic client.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><a href="https://github.com/reminiflux/reminiflux">reminiflux</a></td>
|
||||||
|
<td></td>
|
||||||
|
<td class="Y">✔</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td>
|
||||||
|
<p>Three-pane alternative front-end for Minflux. Does not include functionality for managing feeds. Requires token authentication.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><a href="https://github.com/kucrut/ttrss-reader">Tiny Tiny RSS Reader</a></td>
|
||||||
|
<td></td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td class="Y">✔</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td>
|
||||||
|
<p></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><a href="https://github.com/TheScientist/ttrss-pwa">Tiny Tiny RSS Progressive Web App</a></td>
|
||||||
|
<td></td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td class="Y">✔</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td>
|
||||||
|
<p>Does not (<a href="https://github.com/TheScientist/ttrss-pwa/issues/7">yet</a>) support HTTP authentication. Does not include functionality for managing feeds.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th colspan="7">Desktop</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="https://jangernert.github.io/FeedReader/">FeedReader</a></td>
|
<td><a href="https://jangernert.github.io/FeedReader/">FeedReader</a></td>
|
||||||
<td>Linux</td>
|
<td>Linux</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
<td class="Y">✔</td>
|
<td class="Y">✔</td>
|
||||||
<td class="Y">✔</td>
|
<td class="Y">✔</td>
|
||||||
<td class="N">✘</td>
|
<td class="N">✘</td>
|
||||||
<td>
|
<td>
|
||||||
<p>Excellent reader; one of the best on any platform.</p>
|
<p>Excellent reader; discontinued in favour of NewsFlash.</p>
|
||||||
<p>Not compatible with HTTP authentication when using TT-RSS.</p>
|
<p>Not compatible with HTTP authentication when using TT-RSS.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -33,6 +91,7 @@ The Arsse does not at this time have any first party clients. However, because T
|
||||||
<td><a href="https://lzone.de/liferea/">Liferea</a></td>
|
<td><a href="https://lzone.de/liferea/">Liferea</a></td>
|
||||||
<td>Linux</td>
|
<td>Linux</td>
|
||||||
<td class="N">✘</td>
|
<td class="N">✘</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
<td class="Y">✔</td>
|
<td class="Y">✔</td>
|
||||||
<td class="N">✘</td>
|
<td class="N">✘</td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -44,6 +103,7 @@ The Arsse does not at this time have any first party clients. However, because T
|
||||||
<td>Linux, macOS</td>
|
<td>Linux, macOS</td>
|
||||||
<td class="Y">✔</td>
|
<td class="Y">✔</td>
|
||||||
<td class="Y">✔</td>
|
<td class="Y">✔</td>
|
||||||
|
<td class="Y">✔</td>
|
||||||
<td class="N">✘</td>
|
<td class="N">✘</td>
|
||||||
<td>
|
<td>
|
||||||
<p>Terminal-based client.</p>
|
<p>Terminal-based client.</p>
|
||||||
|
@ -52,11 +112,12 @@ The Arsse does not at this time have any first party clients. However, because T
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="https://gitlab.com/news-flash/news_flash_gtk">NewsFlash</a></td>
|
<td><a href="https://gitlab.com/news-flash/news_flash_gtk">NewsFlash</a></td>
|
||||||
<td>Linux</td>
|
<td>Linux</td>
|
||||||
|
<td class="Y">✔</td>
|
||||||
<td class="N">✘</td>
|
<td class="N">✘</td>
|
||||||
<td class="N">✘</td>
|
<td class="N">✘</td>
|
||||||
<td class="Y">✔</td>
|
<td class="Y">✔</td>
|
||||||
<td>
|
<td>
|
||||||
<p>Successor to FeedReader.</p>
|
<p>Successor to FeedReader. One of the best on any platform</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -64,6 +125,7 @@ The Arsse does not at this time have any first party clients. However, because T
|
||||||
<td>macOS</td>
|
<td>macOS</td>
|
||||||
<td class="N">✘</td>
|
<td class="N">✘</td>
|
||||||
<td class="N">✘</td>
|
<td class="N">✘</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
<td class="Y">✔</td>
|
<td class="Y">✔</td>
|
||||||
<td>
|
<td>
|
||||||
<p>Also available for iOS.</p>
|
<p>Also available for iOS.</p>
|
||||||
|
@ -72,18 +134,20 @@ The Arsse does not at this time have any first party clients. However, because T
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="https://github.com/martinrotter/rssguard/">RSS Guard</a></td>
|
<td><a href="https://github.com/martinrotter/rssguard/">RSS Guard</a></td>
|
||||||
<td>Windows, macOS, Linux</td>
|
<td>Windows, macOS, Linux</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
<td class="Y">✔</td>
|
<td class="Y">✔</td>
|
||||||
<td class="Y">✔</td>
|
<td class="Y">✔</td>
|
||||||
<td class="N">✘</td>
|
<td class="N">✘</td>
|
||||||
<td>
|
<td>
|
||||||
<p>Very basic client; now discontinued.</p>
|
<p>Very basic client.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="https://www.microsoft.com/store/apps/9wzdncrdmbn3">Tiny Tiny RSS Reader</td>
|
<td><a href="https://bitbucket.org/thescientist/tiny-tiny-rss-wp8-client/src/master/">Tiny Tiny RSS Reader</td>
|
||||||
<td>Windows</td>
|
<td>Windows</td>
|
||||||
<td class="N">✘</td>
|
<td class="N">✘</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
<td class="Y">✔</td>
|
<td class="Y">✔</td>
|
||||||
<td class="N">✘</td>
|
<td class="N">✘</td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -93,11 +157,12 @@ The Arsse does not at this time have any first party clients. However, because T
|
||||||
</tbody>
|
</tbody>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<th colspan="6">Mobile</th>
|
<th colspan="7">Mobile</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="https://peterandlinda.com/cloudnews/">CloudNews</a></td>
|
<td><a href="https://peterandlinda.com/cloudnews/">CloudNews</a></td>
|
||||||
<td>iOS</td>
|
<td>iOS</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
<td class="Y">✔</td>
|
<td class="Y">✔</td>
|
||||||
<td class="N">✘</td>
|
<td class="N">✘</td>
|
||||||
<td class="N">✘</td>
|
<td class="N">✘</td>
|
||||||
|
@ -109,6 +174,7 @@ The Arsse does not at this time have any first party clients. However, because T
|
||||||
<td><a href="https://play.google.com/store/apps/details?id=com.seazon.feedme">FeedMe</a></td>
|
<td><a href="https://play.google.com/store/apps/details?id=com.seazon.feedme">FeedMe</a></td>
|
||||||
<td>Android</td>
|
<td>Android</td>
|
||||||
<td class="N">✘</td>
|
<td class="N">✘</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
<td class="Y">✔</td>
|
<td class="Y">✔</td>
|
||||||
<td class="N">✘</td>
|
<td class="N">✘</td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -119,16 +185,48 @@ The Arsse does not at this time have any first party clients. However, because T
|
||||||
<td><a href="http://cocoacake.net/apps/fiery/">Fiery Feeds</a></td>
|
<td><a href="http://cocoacake.net/apps/fiery/">Fiery Feeds</a></td>
|
||||||
<td>iOS</td>
|
<td>iOS</td>
|
||||||
<td class="N">✘</td>
|
<td class="N">✘</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
<td class="Y">✔</td>
|
<td class="Y">✔</td>
|
||||||
<td class="Y">✔</td>
|
<td class="Y">✔</td>
|
||||||
<td>
|
<td>
|
||||||
<p>Rentalware - For the software to be usable (you can't even add feeds otherwise) a subscription fee must be paid.</p>
|
<p>Rentalware - For the software to be usable (you can't even add feeds otherwise) a subscription fee must be paid.</p>
|
||||||
|
<p>Support HTTP authentication with Fever.</p>
|
||||||
<p>Currently keeps showing items in the unread badge which have already been read.</p>
|
<p>Currently keeps showing items in the unread badge which have already been read.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><a href="https://github.com/fbarthelery/geekttrss">Geekttrss</a></td>
|
||||||
|
<td>Android</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td class="Y">✔</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td>
|
||||||
|
<p></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><a href="https://play.google.com/store/apps/details?id=com.constantin.microflux">Microflux for Miniflux</a></td>
|
||||||
|
<td>Android</td>
|
||||||
|
<td class="Y">✔</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><a href="https://play.google.com/store/apps/details?id=mobi.newsjet.rss">NewsJet RSS</a></td>
|
||||||
|
<td>Android</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td class="Y">✔</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="https://github.com/SimonSchubert/NewsOut">Newsout</a></td>
|
<td><a href="https://github.com/SimonSchubert/NewsOut">Newsout</a></td>
|
||||||
<td>Android, iOS</td>
|
<td>Android, iOS</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
<td class="Y">✔</td>
|
<td class="Y">✔</td>
|
||||||
<td class="N">✘</td>
|
<td class="N">✘</td>
|
||||||
<td class="N">✘</td>
|
<td class="N">✘</td>
|
||||||
|
@ -139,6 +237,7 @@ The Arsse does not at this time have any first party clients. However, because T
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="https://github.com/nextcloud/news-android/">Nextcloud News</a></td>
|
<td><a href="https://github.com/nextcloud/news-android/">Nextcloud News</a></td>
|
||||||
<td>Android</td>
|
<td>Android</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
<td class="Y">✔</td>
|
<td class="Y">✔</td>
|
||||||
<td class="N">✘</td>
|
<td class="N">✘</td>
|
||||||
<td class="N">✘</td>
|
<td class="N">✘</td>
|
||||||
|
@ -149,6 +248,7 @@ The Arsse does not at this time have any first party clients. However, because T
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="https://github.com/schaal/ocreader/">OCReader</a></td>
|
<td><a href="https://github.com/schaal/ocreader/">OCReader</a></td>
|
||||||
<td>Android</td>
|
<td>Android</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
<td class="Y">✔</td>
|
<td class="Y">✔</td>
|
||||||
<td class="N">✘</td>
|
<td class="N">✘</td>
|
||||||
<td class="N">✘</td>
|
<td class="N">✘</td>
|
||||||
|
@ -159,16 +259,38 @@ The Arsse does not at this time have any first party clients. However, because T
|
||||||
<td>Android</td>
|
<td>Android</td>
|
||||||
<td class="N">✘</td>
|
<td class="N">✘</td>
|
||||||
<td class="N">✘</td>
|
<td class="N">✘</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
<td class="Y">✔</td>
|
<td class="Y">✔</td>
|
||||||
<td>
|
<td>
|
||||||
<p>Fetches favicons independently.</p>
|
<p>Fetches favicons independently.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><a href="https://github.com/readrops/Readrops">Readrops</a></td>
|
||||||
|
<td>Android</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td class="Y">✔</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><a href="https://github.com/ali322/reed">Reed</a></td>
|
||||||
|
<td>Android</td>
|
||||||
|
<td class="Y">✔</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td>
|
||||||
|
<p>Binaries only available from GitHub.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="https://reeder.app/">Reeder</a></td>
|
<td><a href="https://reeder.app/">Reeder</a></td>
|
||||||
<td>iOS</td>
|
<td>iOS</td>
|
||||||
<td class="N">✘</td>
|
<td class="N">✘</td>
|
||||||
<td class="N">✘</td>
|
<td class="N">✘</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
<td class="Y">✔</td>
|
<td class="Y">✔</td>
|
||||||
<td>
|
<td>
|
||||||
<p>Also available for macOS.</p>
|
<p>Also available for macOS.</p>
|
||||||
|
@ -178,6 +300,7 @@ The Arsse does not at this time have any first party clients. However, because T
|
||||||
<td><a href="http://tt-rss.org/">Tiny Tiny RSS</a></td>
|
<td><a href="http://tt-rss.org/">Tiny Tiny RSS</a></td>
|
||||||
<td>Android</td>
|
<td>Android</td>
|
||||||
<td class="N">✘</td>
|
<td class="N">✘</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
<td class="Y">✔</td>
|
<td class="Y">✔</td>
|
||||||
<td class="N">✘</td>
|
<td class="N">✘</td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -188,6 +311,7 @@ The Arsse does not at this time have any first party clients. However, because T
|
||||||
<td><a href="http://github.com/nilsbraden/ttrss-reader-fork/">TTRSS-Reader</a></td>
|
<td><a href="http://github.com/nilsbraden/ttrss-reader-fork/">TTRSS-Reader</a></td>
|
||||||
<td>Android</td>
|
<td>Android</td>
|
||||||
<td class="N">✘</td>
|
<td class="N">✘</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
<td class="Y">✔</td>
|
<td class="Y">✔</td>
|
||||||
<td class="N">✘</td>
|
<td class="N">✘</td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -199,6 +323,7 @@ The Arsse does not at this time have any first party clients. However, because T
|
||||||
<td>iOS</td>
|
<td>iOS</td>
|
||||||
<td class="N">✘</td>
|
<td class="N">✘</td>
|
||||||
<td class="N">✘</td>
|
<td class="N">✘</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
<td class="Y">✔</td>
|
<td class="Y">✔</td>
|
||||||
<td>
|
<td>
|
||||||
<p>Trialware with one-time purchase.</p>
|
<p>Trialware with one-time purchase.</p>
|
||||||
|
@ -214,10 +339,11 @@ The Arsse does not at this time have any first party clients. However, because T
|
||||||
<tr>
|
<tr>
|
||||||
<th rowspan="2">Name</th>
|
<th rowspan="2">Name</th>
|
||||||
<th rowspan="2">OS</th>
|
<th rowspan="2">OS</th>
|
||||||
<th colspan="3">Protocol</th>
|
<th colspan="4">Protocol</th>
|
||||||
<th rowspan="2">Notes</th>
|
<th rowspan="2">Notes</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th>Miniflux</th>
|
||||||
<th>Nextcloud News</th>
|
<th>Nextcloud News</th>
|
||||||
<th>Tiny Tiny RSS</th>
|
<th>Tiny Tiny RSS</th>
|
||||||
<th>Fever</th>
|
<th>Fever</th>
|
||||||
|
@ -228,6 +354,7 @@ The Arsse does not at this time have any first party clients. However, because T
|
||||||
<td><a href="https://github.com/jeena/feedthemonkey">FeedTheMonkey</a></td>
|
<td><a href="https://github.com/jeena/feedthemonkey">FeedTheMonkey</a></td>
|
||||||
<td>Linux</td>
|
<td>Linux</td>
|
||||||
<td class="N">✘</td>
|
<td class="N">✘</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
<td class="Y">✔</td>
|
<td class="Y">✔</td>
|
||||||
<td class="N">✘</td>
|
<td class="N">✘</td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -235,13 +362,66 @@ The Arsse does not at this time have any first party clients. However, because T
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="https://open-store.io/app/newsie.martinferretti">Newsie</a></td>
|
<td><a href="https://github.com/Huessenbergnetz/Fuoten">Fuoten</a></td>
|
||||||
<td>Ubuntu Touch</td>
|
<td>Sailfish</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td class="Y">✔</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td>
|
||||||
|
<p></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><a href="https://github.com/mkiol/kaktus">Kaktus</a></td>
|
||||||
|
<td>Sailfish, BlackBerry</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td class="Y">✔</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td>
|
||||||
|
<p></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!--
|
||||||
|
<tr>
|
||||||
|
<td><a href="https://github.com/fabienheureux/liseur">Liseur</a></td>
|
||||||
|
<td>Windows?</td>
|
||||||
|
<td class="Y">✔</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td>
|
||||||
|
<p>Level of functionality unclear.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><a href="https://github.com/jakobend/maxiflux">maxiflux</a></td>
|
||||||
|
<td>Web</td>
|
||||||
|
<td class="Y">✔</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td>Level of functionality unclear.</td>
|
||||||
|
</tr>
|
||||||
|
-->
|
||||||
|
<tr>
|
||||||
|
<td><a href="https://github.com/DocMarty84/miniflutt">Miniflutt</a></td>
|
||||||
|
<td>Android</td>
|
||||||
|
<td class="Y">✔</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td>Does not display articles (<a href="https://github.com/DocMarty84/miniflutt/issues/3">see bug</a>)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><a href="https://open-store.io/app/newsie.martinferretti">Newsie</a></td>
|
||||||
|
<td>Ubuntu Touch</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
<td class="Y">✔</td>
|
<td class="Y">✔</td>
|
||||||
<td class="N">✘</td>
|
<td class="N">✘</td>
|
||||||
<td class="N">✘</td>
|
<td class="N">✘</td>
|
||||||
<td>
|
<td>
|
||||||
<p>Does not support HTTP authentication.</p>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -249,6 +429,7 @@ The Arsse does not at this time have any first party clients. However, because T
|
||||||
<td>macOS</td>
|
<td>macOS</td>
|
||||||
<td class="N">✘</td>
|
<td class="N">✘</td>
|
||||||
<td class="N">✘</td>
|
<td class="N">✘</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
<td class="Y">✔</td>
|
<td class="Y">✔</td>
|
||||||
<td>
|
<td>
|
||||||
<p>Requires purchase. Presumed to work.</p>
|
<p>Requires purchase. Presumed to work.</p>
|
||||||
|
@ -259,6 +440,7 @@ The Arsse does not at this time have any first party clients. However, because T
|
||||||
<td>Windows</td>
|
<td>Windows</td>
|
||||||
<td class="N">✘</td>
|
<td class="N">✘</td>
|
||||||
<td class="N">✘</td>
|
<td class="N">✘</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
<td class="Y">✔</td>
|
<td class="Y">✔</td>
|
||||||
<td>
|
<td>
|
||||||
<p>Requires manual configuration.</p>
|
<p>Requires manual configuration.</p>
|
||||||
|
@ -268,11 +450,23 @@ The Arsse does not at this time have any first party clients. However, because T
|
||||||
<td><a href="http://www.pluchon.com/en/tiny_reader_rss.php">tiny Reader RSS</a></td>
|
<td><a href="http://www.pluchon.com/en/tiny_reader_rss.php">tiny Reader RSS</a></td>
|
||||||
<td>iOS</td>
|
<td>iOS</td>
|
||||||
<td class="N">✘</td>
|
<td class="N">✘</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
<td class="Y">✔</td>
|
<td class="Y">✔</td>
|
||||||
<td class="N">✘</td>
|
<td class="N">✘</td>
|
||||||
<td>
|
<td>
|
||||||
<p>Does not support HTTP authentication.</p>
|
<p>Does not support HTTP authentication.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><a href="https://github.com/cnlpete/ttrss">ttrss</a></td>
|
||||||
|
<td>Sailfish</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td class="Y">✔</td>
|
||||||
|
<td class="N">✘</td>
|
||||||
|
<td>
|
||||||
|
<p></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
2
docs/theme/arsse/arsse.css
vendored
2
docs/theme/arsse/arsse.css
vendored
File diff suppressed because one or more lines are too long
4
docs/theme/src/arsse.scss
vendored
4
docs/theme/src/arsse.scss
vendored
|
@ -245,12 +245,12 @@ ul.TableOfContents {
|
||||||
}
|
}
|
||||||
|
|
||||||
thead tr + tr th {
|
thead tr + tr th {
|
||||||
width: 16.66%;
|
width: 12%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody td {
|
tbody td {
|
||||||
&:nth-child(3), &:nth-child(4), &:nth-child(5) {
|
&:nth-child(3), &:nth-child(4), &:nth-child(5), &:nth-child(6) {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ abstract class AbstractException extends \Exception {
|
||||||
"Exception.arrayEmpty" => 10002,
|
"Exception.arrayEmpty" => 10002,
|
||||||
"ExceptionType.strictFailure" => 10011,
|
"ExceptionType.strictFailure" => 10011,
|
||||||
"ExceptionType.typeUnknown" => 10012,
|
"ExceptionType.typeUnknown" => 10012,
|
||||||
|
"Exception.extMissing" => 10021,
|
||||||
"Lang/Exception.defaultFileMissing" => 10101,
|
"Lang/Exception.defaultFileMissing" => 10101,
|
||||||
"Lang/Exception.fileMissing" => 10102,
|
"Lang/Exception.fileMissing" => 10102,
|
||||||
"Lang/Exception.fileUnreadable" => 10103,
|
"Lang/Exception.fileUnreadable" => 10103,
|
||||||
|
@ -46,6 +47,7 @@ abstract class AbstractException extends \Exception {
|
||||||
"Db/Exception.savepointStale" => 10227,
|
"Db/Exception.savepointStale" => 10227,
|
||||||
"Db/Exception.resultReused" => 10228,
|
"Db/Exception.resultReused" => 10228,
|
||||||
"Db/ExceptionRetry.schemaChange" => 10229,
|
"Db/ExceptionRetry.schemaChange" => 10229,
|
||||||
|
"Db/ExceptionInput.invalidValue" => 10230,
|
||||||
"Db/ExceptionInput.missing" => 10231,
|
"Db/ExceptionInput.missing" => 10231,
|
||||||
"Db/ExceptionInput.whitespace" => 10232,
|
"Db/ExceptionInput.whitespace" => 10232,
|
||||||
"Db/ExceptionInput.tooLong" => 10233,
|
"Db/ExceptionInput.tooLong" => 10233,
|
||||||
|
@ -68,13 +70,15 @@ abstract class AbstractException extends \Exception {
|
||||||
"Conf/Exception.typeMismatch" => 10311,
|
"Conf/Exception.typeMismatch" => 10311,
|
||||||
"Conf/Exception.semanticMismatch" => 10312,
|
"Conf/Exception.semanticMismatch" => 10312,
|
||||||
"Conf/Exception.ambiguousDefault" => 10313,
|
"Conf/Exception.ambiguousDefault" => 10313,
|
||||||
"User/Exception.functionNotImplemented" => 10401,
|
|
||||||
"User/Exception.doesNotExist" => 10402,
|
|
||||||
"User/Exception.alreadyExists" => 10403,
|
|
||||||
"User/Exception.authMissing" => 10411,
|
"User/Exception.authMissing" => 10411,
|
||||||
"User/Exception.authFailed" => 10412,
|
"User/Exception.authFailed" => 10412,
|
||||||
"User/ExceptionAuthz.notAuthorized" => 10421,
|
"User/ExceptionConflict.doesNotExist" => 10402,
|
||||||
|
"User/ExceptionConflict.alreadyExists" => 10403,
|
||||||
"User/ExceptionSession.invalid" => 10431,
|
"User/ExceptionSession.invalid" => 10431,
|
||||||
|
"User/ExceptionInput.invalidTimezone" => 10441,
|
||||||
|
"User/ExceptionInput.invalidValue" => 10442,
|
||||||
|
"User/ExceptionInput.invalidNonZeroInteger" => 10443,
|
||||||
|
"User/ExceptionInput.invalidUsername" => 10444,
|
||||||
"Feed/Exception.internalError" => 10500,
|
"Feed/Exception.internalError" => 10500,
|
||||||
"Feed/Exception.invalidCertificate" => 10501,
|
"Feed/Exception.invalidCertificate" => 10501,
|
||||||
"Feed/Exception.invalidUrl" => 10502,
|
"Feed/Exception.invalidUrl" => 10502,
|
||||||
|
@ -98,9 +102,27 @@ abstract class AbstractException extends \Exception {
|
||||||
"ImportExport/Exception.invalidFolderName" => 10613,
|
"ImportExport/Exception.invalidFolderName" => 10613,
|
||||||
"ImportExport/Exception.invalidFolderCopy" => 10614,
|
"ImportExport/Exception.invalidFolderCopy" => 10614,
|
||||||
"ImportExport/Exception.invalidTagName" => 10615,
|
"ImportExport/Exception.invalidTagName" => 10615,
|
||||||
|
"Rule/Exception.invalidPattern" => 10701,
|
||||||
|
"Service/Exception.pidNotFile" => 10801,
|
||||||
|
"Service/Exception.pidDirMissing" => 10802,
|
||||||
|
"Service/Exception.pidDirUnresolvable" => 10803,
|
||||||
|
"Service/Exception.pidUnusable" => 10804,
|
||||||
|
"Service/Exception.pidUnreadable" => 10805,
|
||||||
|
"Service/Exception.pidUnwritable" => 10806,
|
||||||
|
"Service/Exception.pidUncreatable" => 10807,
|
||||||
|
"Service/Exception.pidCorrupt" => 10808,
|
||||||
|
"Service/Exception.pidDuplicate" => 10809,
|
||||||
|
"Service/Exception.pidLocked" => 10810,
|
||||||
|
"Service/Exception.pidInaccessible" => 10811,
|
||||||
|
"Service/Exception.forkFailed" => 10812,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
protected $symbol;
|
||||||
|
protected $params;
|
||||||
|
|
||||||
public function __construct(string $msgID = "", $vars = null, \Throwable $e = null) {
|
public function __construct(string $msgID = "", $vars = null, \Throwable $e = null) {
|
||||||
|
$this->symbol = $msgID;
|
||||||
|
$this->params = $vars ?? [];
|
||||||
if ($msgID === "") {
|
if ($msgID === "") {
|
||||||
$msg = "Exception.unknown";
|
$msg = "Exception.unknown";
|
||||||
$code = 10000;
|
$code = 10000;
|
||||||
|
@ -117,4 +139,12 @@ abstract class AbstractException extends \Exception {
|
||||||
}
|
}
|
||||||
parent::__construct($msg, $code, $e);
|
parent::__construct($msg, $code, $e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getSymbol(): string {
|
||||||
|
return $this->symbol;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getParams(): array {
|
||||||
|
return $this->params;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,8 +7,19 @@ declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse;
|
namespace JKingWeb\Arsse;
|
||||||
|
|
||||||
class Arsse {
|
class Arsse {
|
||||||
public const VERSION = "0.8.5";
|
public const VERSION = "0.10.0";
|
||||||
|
public const REQUIRED_EXTENSIONS = [
|
||||||
|
"intl", // as this extension is required to prepare formatted messages, its absence will throw a distinct English-only exception
|
||||||
|
"dom",
|
||||||
|
"filter",
|
||||||
|
"json", // part of the PHP core since version 8.0
|
||||||
|
"hash", // part of the PHP core since version 7.4
|
||||||
|
"simplexml", // required by PicoFeed only
|
||||||
|
"iconv", // required by PicoFeed only
|
||||||
|
];
|
||||||
|
|
||||||
|
/** @var Factory */
|
||||||
|
public static $obj;
|
||||||
/** @var Lang */
|
/** @var Lang */
|
||||||
public static $lang;
|
public static $lang;
|
||||||
/** @var Conf */
|
/** @var Conf */
|
||||||
|
@ -18,11 +29,33 @@ class Arsse {
|
||||||
/** @var User */
|
/** @var User */
|
||||||
public static $user;
|
public static $user;
|
||||||
|
|
||||||
|
/** @codeCoverageIgnore */
|
||||||
|
public static function bootstrap(): void {
|
||||||
|
$conf = file_exists(BASE."config.php") ? new Conf(BASE."config.php") : new Conf;
|
||||||
|
static::load($conf);
|
||||||
|
}
|
||||||
|
|
||||||
public static function load(Conf $conf): void {
|
public static function load(Conf $conf): void {
|
||||||
|
static::$obj = static::$obj ?? new Factory;
|
||||||
static::$lang = static::$lang ?? new Lang;
|
static::$lang = static::$lang ?? new Lang;
|
||||||
static::$conf = $conf;
|
static::$conf = $conf;
|
||||||
static::$lang->set($conf->lang);
|
static::$lang->set($conf->lang);
|
||||||
static::$db = static::$db ?? new Database;
|
static::$db = static::$db ?? new Database;
|
||||||
static::$user = static::$user ?? new User;
|
static::$user = static::$user ?? new User;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Checks whether the specified extensions are loaded and throws an exception if any are not */
|
||||||
|
public static function checkExtensions(string ...$ext): void {
|
||||||
|
$missing = [];
|
||||||
|
foreach ($ext as $e) {
|
||||||
|
if (!extension_loaded($e)) {
|
||||||
|
$missing[] = $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($missing) {
|
||||||
|
$total = sizeof($missing);
|
||||||
|
$first = $missing[0];
|
||||||
|
throw new Exception("extMissing", ['first' => $first, 'total' => $total]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
308
lib/CLI.php
308
lib/CLI.php
|
@ -8,126 +8,40 @@ namespace JKingWeb\Arsse;
|
||||||
|
|
||||||
use JKingWeb\Arsse\REST\Fever\User as Fever;
|
use JKingWeb\Arsse\REST\Fever\User as Fever;
|
||||||
use JKingWeb\Arsse\ImportExport\OPML;
|
use JKingWeb\Arsse\ImportExport\OPML;
|
||||||
|
use JKingWeb\Arsse\REST\Miniflux\Token as Miniflux;
|
||||||
|
use JKingWeb\Arsse\Service\Daemon;
|
||||||
|
|
||||||
class CLI {
|
class CLI {
|
||||||
public const USAGE = <<<USAGE_TEXT
|
public const USAGE = <<<USAGE_TEXT
|
||||||
Usage:
|
Usage:
|
||||||
arsse.php daemon
|
arsse.php user [list]
|
||||||
|
arsse.php user add <username> [<password>] [--admin]
|
||||||
|
arsse.php user remove <username>
|
||||||
|
arsse.php user show <username>
|
||||||
|
arsse.php user set <username> <property> <value>
|
||||||
|
arsse.php user unset <username> <property>
|
||||||
|
arsse.php user set-pass <username> [<password>] [--fever]
|
||||||
|
arsse.php user unset-pass <username> [--fever]
|
||||||
|
arsse.php user auth <username> <password> [--fever]
|
||||||
|
arsse.php token list <username>
|
||||||
|
arsse.php token create <username> [<label>]
|
||||||
|
arsse.php token revoke <username> [<token>]
|
||||||
|
arsse.php import <username> [<file>] [-f|--flat] [-r|--replace]
|
||||||
|
arsse.php export <username> [<file>] [-f|--flat]
|
||||||
|
arsse.php daemon [--fork=PIDFILE]
|
||||||
arsse.php feed refresh-all
|
arsse.php feed refresh-all
|
||||||
arsse.php feed refresh <n>
|
arsse.php feed refresh <n>
|
||||||
arsse.php conf save-defaults [<file>]
|
arsse.php conf save-defaults [<file>]
|
||||||
arsse.php user [list]
|
|
||||||
arsse.php user add <username> [<password>]
|
|
||||||
arsse.php user remove <username>
|
|
||||||
arsse.php user set-pass <username> [<password>]
|
|
||||||
[--oldpass=<pass>] [--fever]
|
|
||||||
arsse.php user unset-pass <username>
|
|
||||||
[--oldpass=<pass>] [--fever]
|
|
||||||
arsse.php user auth <username> <password> [--fever]
|
|
||||||
arsse.php import <username> [<file>]
|
|
||||||
[-f | --flat] [-r | --replace]
|
|
||||||
arsse.php export <username> [<file>]
|
|
||||||
[-f | --flat]
|
|
||||||
arsse.php --version
|
arsse.php --version
|
||||||
arsse.php -h | --help
|
arsse.php -h|--help
|
||||||
|
|
||||||
The Arsse command-line interface can be used to perform various administrative
|
The Arsse command-line interface can be used to perform various administrative
|
||||||
tasks such as starting the newsfeed refresh service, managing users, and
|
tasks such as starting the newsfeed refresh service, managing users, and
|
||||||
importing or exporting data.
|
importing or exporting data.
|
||||||
|
|
||||||
Commands:
|
See the manual page for more details:
|
||||||
|
|
||||||
daemon
|
man arsse
|
||||||
|
|
||||||
Starts the newsfeed refreshing service, which will refresh stale feeds at
|
|
||||||
the configured interval automatically.
|
|
||||||
|
|
||||||
feed refresh-all
|
|
||||||
|
|
||||||
Refreshes any stale feeds once, then exits. This performs the same
|
|
||||||
function as the daemon command without looping; this is useful if use of
|
|
||||||
a scheduler such a cron is preferred over a persitent service.
|
|
||||||
|
|
||||||
feed refresh <n>
|
|
||||||
|
|
||||||
Refreshes a single feed by numeric ID. This is principally for internal
|
|
||||||
use as the feed ID numbers are not usually exposed to the user.
|
|
||||||
|
|
||||||
conf save-defaults [<file>]
|
|
||||||
|
|
||||||
Prints default configuration parameters to standard output, or to <file>
|
|
||||||
if specified. Each parameter is annotated with a short description of its
|
|
||||||
purpose and usage.
|
|
||||||
|
|
||||||
user [list]
|
|
||||||
|
|
||||||
Prints a list of all existing users, one per line.
|
|
||||||
|
|
||||||
user add <username> [<password>]
|
|
||||||
|
|
||||||
Adds the user specified by <username>, with the provided password
|
|
||||||
<password>. If no password is specified, a random password will be
|
|
||||||
generated and printed to standard output.
|
|
||||||
|
|
||||||
user remove <username>
|
|
||||||
|
|
||||||
Removes the user specified by <username>. Data related to the user,
|
|
||||||
including folders and subscriptions, are immediately deleted. Feeds to
|
|
||||||
which the user was subscribed will be retained and refreshed until the
|
|
||||||
configured retention time elapses.
|
|
||||||
|
|
||||||
user set-pass <username> [<password>]
|
|
||||||
|
|
||||||
Changes <username>'s password to <password>. If no password is specified,
|
|
||||||
a random password will be generated and printed to standard output.
|
|
||||||
|
|
||||||
The --oldpass=<pass> option can be used to supply a user's exiting
|
|
||||||
password if this is required by the authentication driver to change a
|
|
||||||
password. Currently this is not used by any existing driver.
|
|
||||||
|
|
||||||
The --fever option sets a user's Fever protocol password instead of their
|
|
||||||
general password. As Fever requires that passwords be stored insecurely,
|
|
||||||
users do not have Fever passwords by default, and logging in to the Fever
|
|
||||||
protocol is disabled until a password is set. It is highly recommended
|
|
||||||
that a user's Fever password be different from their general password.
|
|
||||||
|
|
||||||
user unset-pass <username>
|
|
||||||
|
|
||||||
Unsets a user's password, effectively disabling their account. As with
|
|
||||||
password setting, the --oldpass and --fever options may be used.
|
|
||||||
|
|
||||||
user auth <username> <password>
|
|
||||||
|
|
||||||
Tests logging in as <username> with password <password>. This only checks
|
|
||||||
that the user's password is correctly recognized; it has no side effects.
|
|
||||||
|
|
||||||
The --fever option may be used to test the user's Fever protocol password,
|
|
||||||
if any.
|
|
||||||
|
|
||||||
import <username> [<file>]
|
|
||||||
|
|
||||||
Imports the feeds, folders, and tags found in the OPML formatted <file>
|
|
||||||
into the account of <username>. If no file is specified, data is instead
|
|
||||||
read from standard input.
|
|
||||||
|
|
||||||
The --replace option interprets the OPML file as the list of all desired
|
|
||||||
feeds, folders and tags, performing any deletion or moving of existing
|
|
||||||
entries which do not appear in the flle. If this option is not specified,
|
|
||||||
the file is assumed to list desired additions only.
|
|
||||||
|
|
||||||
The --flat option can be used to ignore any folder structures in the file,
|
|
||||||
importing any feeds only into the root folder.
|
|
||||||
|
|
||||||
export <username> [<file>]
|
|
||||||
|
|
||||||
Exports <username>'s feeds, folders, and tags to the OPML file specified
|
|
||||||
by <file>, or standard output if none is provided. Note that due to a
|
|
||||||
limitation of the OPML format, any commas present in tag names will not be
|
|
||||||
retained in the export.
|
|
||||||
|
|
||||||
The --flat option can be used to omit folders from the export. Some OPML
|
|
||||||
implementations may not support folders, or arbitrary nesting; this option
|
|
||||||
may be used when planning to import into such software.
|
|
||||||
USAGE_TEXT;
|
USAGE_TEXT;
|
||||||
|
|
||||||
protected function usage($prog): string {
|
protected function usage($prog): string {
|
||||||
|
@ -135,22 +49,19 @@ USAGE_TEXT;
|
||||||
return str_replace("arsse.php", $prog, self::USAGE);
|
return str_replace("arsse.php", $prog, self::USAGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function command(array $options, $args): string {
|
protected function command($args): string {
|
||||||
foreach ($options as $cmd) {
|
$out = [];
|
||||||
foreach (explode(" ", $cmd) as $part) {
|
foreach ($args as $k => $v) {
|
||||||
if (!$args[$part]) {
|
if (preg_match("/^[a-z]/", $k) && $v === true) {
|
||||||
continue 2;
|
$out[] = $k;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return $cmd;
|
|
||||||
}
|
}
|
||||||
return "";
|
return implode(" ", $out);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @codeCoverageIgnore */
|
/** @codeCoverageIgnore */
|
||||||
protected function loadConf(): bool {
|
protected function loadConf(): bool {
|
||||||
$conf = file_exists(BASE."config.php") ? new Conf(BASE."config.php") : new Conf;
|
Arsse::bootstrap();
|
||||||
Arsse::load($conf);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -168,40 +79,95 @@ USAGE_TEXT;
|
||||||
'help' => false,
|
'help' => false,
|
||||||
]);
|
]);
|
||||||
try {
|
try {
|
||||||
$cmd = $this->command(["-h", "--help", "--version", "daemon", "feed refresh", "feed refresh-all", "conf save-defaults", "user", "export", "import"], $args);
|
// ensure the require extensions are loaded
|
||||||
if ($cmd && !in_array($cmd, ["-h", "--help", "--version", "conf save-defaults"])) {
|
Arsse::checkExtensions(...Arsse::REQUIRED_EXTENSIONS);
|
||||||
// only certain commands don't require configuration to be loaded
|
// reconstitute multi-token commands (e.g. user add) into a single string
|
||||||
|
$cmd = $this->command($args);
|
||||||
|
if ($cmd && !in_array($cmd, ["", "conf save-defaults", "daemon"])) {
|
||||||
|
// only certain commands don't require configuration to be loaded; daemon loads configuration after forking (if applicable)
|
||||||
$this->loadConf();
|
$this->loadConf();
|
||||||
}
|
}
|
||||||
|
// run the requested command
|
||||||
switch ($cmd) {
|
switch ($cmd) {
|
||||||
case "-h":
|
case "":
|
||||||
case "--help":
|
if ($args['--version']) {
|
||||||
echo $this->usage($argv0).\PHP_EOL;
|
echo Arsse::VERSION.\PHP_EOL;
|
||||||
return 0;
|
} elseif ($args['--help'] || $args['-h']) {
|
||||||
case "--version":
|
echo $this->usage($argv0).\PHP_EOL;
|
||||||
echo Arsse::VERSION.\PHP_EOL;
|
}
|
||||||
return 0;
|
return 0;
|
||||||
case "daemon":
|
case "daemon":
|
||||||
$this->getInstance(Service::class)->watch(true);
|
if ($args['--fork'] !== null) {
|
||||||
|
return $this->serviceFork($args['--fork']);
|
||||||
|
} else {
|
||||||
|
$this->loadConf();
|
||||||
|
Arsse::$obj->get(Service::class)->watch(true);
|
||||||
|
}
|
||||||
return 0;
|
return 0;
|
||||||
case "feed refresh":
|
case "feed refresh":
|
||||||
return (int) !Arsse::$db->feedUpdate((int) $args['<n>'], true);
|
return (int) !Arsse::$db->feedUpdate((int) $args['<n>'], true);
|
||||||
case "feed refresh-all":
|
case "feed refresh-all":
|
||||||
$this->getInstance(Service::class)->watch(false);
|
Arsse::$obj->get(Service::class)->watch(false);
|
||||||
return 0;
|
return 0;
|
||||||
case "conf save-defaults":
|
case "conf save-defaults":
|
||||||
$file = $this->resolveFile($args['<file>'], "w");
|
$file = $this->resolveFile($args['<file>'], "w");
|
||||||
return (int) !$this->getInstance(Conf::class)->exportFile($file, true);
|
return (int) !Arsse::$obj->get(Conf::class)->exportFile($file, true);
|
||||||
case "user":
|
|
||||||
return $this->userManage($args);
|
|
||||||
case "export":
|
case "export":
|
||||||
$u = $args['<username>'];
|
$u = $args['<username>'];
|
||||||
$file = $this->resolveFile($args['<file>'], "w");
|
$file = $this->resolveFile($args['<file>'], "w");
|
||||||
return (int) !$this->getInstance(OPML::class)->exportFile($file, $u, ($args['--flat'] || $args['-f']));
|
return (int) !Arsse::$obj->get(OPML::class)->exportFile($file, $u, ($args['--flat'] || $args['-f']));
|
||||||
case "import":
|
case "import":
|
||||||
$u = $args['<username>'];
|
$u = $args['<username>'];
|
||||||
$file = $this->resolveFile($args['<file>'], "r");
|
$file = $this->resolveFile($args['<file>'], "r");
|
||||||
return (int) !$this->getInstance(OPML::class)->importFile($file, $u, ($args['--flat'] || $args['-f']), ($args['--replace'] || $args['-r']));
|
return (int) !Arsse::$obj->get(OPML::class)->importFile($file, $u, ($args['--flat'] || $args['-f']), ($args['--replace'] || $args['-r']));
|
||||||
|
case "token list":
|
||||||
|
case "list token": // command reconstruction yields this order for "token list" command
|
||||||
|
return $this->tokenList($args['<username>']);
|
||||||
|
case "token create":
|
||||||
|
echo Arsse::$obj->get(Miniflux::class)->tokenGenerate($args['<username>'], $args['<label>']).\PHP_EOL;
|
||||||
|
return 0;
|
||||||
|
case "token revoke":
|
||||||
|
Arsse::$db->tokenRevoke($args['<username>'], "miniflux.login", $args['<token>']);
|
||||||
|
return 0;
|
||||||
|
case "user add":
|
||||||
|
$out = $this->userAddOrSetPassword("add", $args["<username>"], $args["<password>"]);
|
||||||
|
if ($args['--admin']) {
|
||||||
|
Arsse::$user->propertiesSet($args["<username>"], ['admin' => true]);
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
|
case "user set-pass":
|
||||||
|
if ($args['--fever']) {
|
||||||
|
$passwd = Arsse::$obj->get(Fever::class)->register($args["<username>"], $args["<password>"]);
|
||||||
|
if (is_null($args["<password>"])) {
|
||||||
|
echo $passwd.\PHP_EOL;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
return $this->userAddOrSetPassword("passwordSet", $args["<username>"], $args["<password>"]);
|
||||||
|
}
|
||||||
|
// no break
|
||||||
|
case "user unset-pass":
|
||||||
|
if ($args['--fever']) {
|
||||||
|
Arsse::$obj->get(Fever::class)->unregister($args["<username>"]);
|
||||||
|
} else {
|
||||||
|
Arsse::$user->passwordUnset($args["<username>"]);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
case "user remove":
|
||||||
|
return (int) !Arsse::$user->remove($args["<username>"]);
|
||||||
|
case "user show":
|
||||||
|
return $this->userShowProperties($args["<username>"]);
|
||||||
|
case "user set":
|
||||||
|
return (int) !Arsse::$user->propertiesSet($args["<username>"], [$args["<property>"] => $args["<value>"]]);
|
||||||
|
case "user unset":
|
||||||
|
return (int) !Arsse::$user->propertiesSet($args["<username>"], [$args["<property>"] => null]);
|
||||||
|
case "user auth":
|
||||||
|
return $this->userAuthenticate($args["<username>"], $args["<password>"], $args["--fever"]);
|
||||||
|
case "user list":
|
||||||
|
case "user":
|
||||||
|
return $this->userList();
|
||||||
|
default:
|
||||||
|
throw new Exception("constantUnknown", $cmd); // @codeCoverageIgnore
|
||||||
}
|
}
|
||||||
} catch (AbstractException $e) {
|
} catch (AbstractException $e) {
|
||||||
$this->logError($e->getMessage());
|
$this->logError($e->getMessage());
|
||||||
|
@ -214,44 +180,21 @@ USAGE_TEXT;
|
||||||
fwrite(STDERR, $msg.\PHP_EOL);
|
fwrite(STDERR, $msg.\PHP_EOL);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @codeCoverageIgnore */
|
protected function serviceFork(string $pidfile): int {
|
||||||
protected function getInstance(string $class) {
|
// initialize the object factory
|
||||||
return new $class;
|
Arsse::$obj = Arsse::$obj ?? new Factory;
|
||||||
}
|
// create a Daemon object which contains various helper functions
|
||||||
|
$daemon = Arsse::$obj->get(Daemon::class);
|
||||||
protected function userManage($args): int {
|
// resolve the PID file to its absolute path; this also checks its readability and writability
|
||||||
$cmd = $this->command(["add", "remove", "set-pass", "unset-pass", "list", "auth"], $args);
|
$pidfile = $daemon->checkPIDFilePath($pidfile);
|
||||||
switch ($cmd) {
|
// daemonize
|
||||||
case "add":
|
$daemon->fork($pidfile);
|
||||||
return $this->userAddOrSetPassword("add", $args["<username>"], $args["<password>"]);
|
// start the fetching service as normal
|
||||||
case "set-pass":
|
$this->loadConf();
|
||||||
if ($args['--fever']) {
|
Arsse::$obj->get(Service::class)->watch(true);
|
||||||
$passwd = $this->getInstance(Fever::class)->register($args["<username>"], $args["<password>"]);
|
// after the service has been shut down, delete the PID file and exit cleanly
|
||||||
if (is_null($args["<password>"])) {
|
unlink($pidfile);
|
||||||
echo $passwd.\PHP_EOL;
|
return 0;
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
} else {
|
|
||||||
return $this->userAddOrSetPassword("passwordSet", $args["<username>"], $args["<password>"], $args["--oldpass"]);
|
|
||||||
}
|
|
||||||
// no break
|
|
||||||
case "unset-pass":
|
|
||||||
if ($args['--fever']) {
|
|
||||||
$this->getInstance(Fever::class)->unregister($args["<username>"]);
|
|
||||||
} else {
|
|
||||||
Arsse::$user->passwordUnset($args["<username>"], $args["--oldpass"]);
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
case "remove":
|
|
||||||
return (int) !Arsse::$user->remove($args["<username>"]);
|
|
||||||
case "auth":
|
|
||||||
return $this->userAuthenticate($args["<username>"], $args["<password>"], $args["--fever"]);
|
|
||||||
case "list":
|
|
||||||
case "":
|
|
||||||
return $this->userList();
|
|
||||||
default:
|
|
||||||
throw new Exception("constantUnknown", $cmd); // @codeCoverageIgnore
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function userAddOrSetPassword(string $method, string $user, string $password = null, string $oldpass = null): int {
|
protected function userAddOrSetPassword(string $method, string $user, string $password = null, string $oldpass = null): int {
|
||||||
|
@ -271,7 +214,7 @@ USAGE_TEXT;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function userAuthenticate(string $user, string $password, bool $fever = false): int {
|
protected function userAuthenticate(string $user, string $password, bool $fever = false): int {
|
||||||
$result = $fever ? $this->getInstance(Fever::class)->authenticate($user, $password) : Arsse::$user->auth($user, $password);
|
$result = $fever ? Arsse::$obj->get(Fever::class)->authenticate($user, $password) : Arsse::$user->auth($user, $password);
|
||||||
if ($result) {
|
if ($result) {
|
||||||
echo Arsse::$lang->msg("CLI.Auth.Success").\PHP_EOL;
|
echo Arsse::$lang->msg("CLI.Auth.Success").\PHP_EOL;
|
||||||
return 0;
|
return 0;
|
||||||
|
@ -280,4 +223,27 @@ USAGE_TEXT;
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function userShowProperties(string $user): int {
|
||||||
|
$data = Arsse::$user->propertiesGet($user);
|
||||||
|
$len = array_reduce(array_keys($data), function($carry, $item) {
|
||||||
|
return max($carry, strlen($item));
|
||||||
|
}, 0) + 2;
|
||||||
|
foreach ($data as $k => $v) {
|
||||||
|
echo str_pad($k, $len, " ");
|
||||||
|
echo var_export($v, true).\PHP_EOL;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tokenList(string $user): int {
|
||||||
|
$list = Arsse::$obj->get(Miniflux::class)->tokenList($user);
|
||||||
|
usort($list, function($v1, $v2) {
|
||||||
|
return $v1['label'] <=> $v2['label'];
|
||||||
|
});
|
||||||
|
foreach ($list as $t) {
|
||||||
|
echo $t['id']." ".$t['label'].\PHP_EOL;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
26
lib/Conf.php
26
lib/Conf.php
|
@ -113,14 +113,6 @@ class Conf {
|
||||||
|
|
||||||
/** @var \DateInterval|null (OBSOLETE) Number of seconds for SQLite to wait before returning a timeout error when trying to acquire a write lock on the database (zero does not wait) */
|
/** @var \DateInterval|null (OBSOLETE) Number of seconds for SQLite to wait before returning a timeout error when trying to acquire a write lock on the database (zero does not wait) */
|
||||||
public $dbSQLite3Timeout = null; // previously 60.0
|
public $dbSQLite3Timeout = null; // previously 60.0
|
||||||
|
|
||||||
protected const TYPE_NAMES = [
|
|
||||||
Value::T_BOOL => "boolean",
|
|
||||||
Value::T_STRING => "string",
|
|
||||||
Value::T_FLOAT => "float",
|
|
||||||
VALUE::T_INT => "integer",
|
|
||||||
Value::T_INTERVAL => "interval",
|
|
||||||
];
|
|
||||||
protected const EXPECTED_TYPES = [
|
protected const EXPECTED_TYPES = [
|
||||||
'dbTimeoutExec' => "double",
|
'dbTimeoutExec' => "double",
|
||||||
'dbTimeoutLock' => "double",
|
'dbTimeoutLock' => "double",
|
||||||
|
@ -128,16 +120,14 @@ class Conf {
|
||||||
'dbSQLite3Timeout' => "double",
|
'dbSQLite3Timeout' => "double",
|
||||||
];
|
];
|
||||||
|
|
||||||
protected static $types = [];
|
protected $types = [];
|
||||||
|
|
||||||
/** Creates a new configuration object
|
/** Creates a new configuration object
|
||||||
* @param string $import_file Optional file to read configuration data from
|
* @param string $import_file Optional file to read configuration data from
|
||||||
* @see self::importFile() */
|
* @see self::importFile() */
|
||||||
public function __construct(string $import_file = "") {
|
public function __construct(string $import_file = "") {
|
||||||
if (!static::$types) {
|
$this->types = $this->propertyDiscover();
|
||||||
static::$types = $this->propertyDiscover();
|
foreach (array_keys($this->types) as $prop) {
|
||||||
}
|
|
||||||
foreach (array_keys(static::$types) as $prop) {
|
|
||||||
$this->$prop = $this->propertyImport($prop, $this->$prop);
|
$this->$prop = $this->propertyImport($prop, $this->$prop);
|
||||||
}
|
}
|
||||||
if ($import_file !== "") {
|
if ($import_file !== "") {
|
||||||
|
@ -273,9 +263,9 @@ class Conf {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function propertyImport(string $key, $value, string $file = "") {
|
protected function propertyImport(string $key, $value, string $file = "") {
|
||||||
$typeName = static::$types[$key]['name'] ?? "mixed";
|
$typeName = $this->types[$key]['name'] ?? "mixed";
|
||||||
$typeConst = static::$types[$key]['const'] ?? Value::T_MIXED;
|
$typeConst = $this->types[$key]['const'] ?? Value::T_MIXED;
|
||||||
$nullable = (int) (bool) (static::$types[$key]['const'] & Value::M_NULL);
|
$nullable = (int) (bool) ($typeConst & Value::M_NULL);
|
||||||
try {
|
try {
|
||||||
if ($typeName === "\\DateInterval") {
|
if ($typeName === "\\DateInterval") {
|
||||||
// date intervals have special handling: if the existing value (ultimately, the default value)
|
// date intervals have special handling: if the existing value (ultimately, the default value)
|
||||||
|
@ -319,8 +309,8 @@ class Conf {
|
||||||
}
|
}
|
||||||
return $value;
|
return $value;
|
||||||
} catch (ExceptionType $e) {
|
} catch (ExceptionType $e) {
|
||||||
$type = static::$types[$key]['const'] & ~(Value::M_STRICT | Value::M_DROP | Value::M_NULL | Value::M_ARRAY);
|
$type = $this->types[$key]['const'] & ~(Value::M_STRICT | Value::M_DROP | Value::M_NULL | Value::M_ARRAY);
|
||||||
throw new Conf\Exception("typeMismatch", ['param' => $key, 'type' => self::TYPE_NAMES[$type], 'file' => $file, 'nullable' => $nullable]);
|
throw new Conf\Exception("typeMismatch", ['param' => $key, 'type' => Value::TYPE_NAMES[$type], 'file' => $file, 'nullable' => $nullable]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ class Context extends ExclusionContext {
|
||||||
public $offset = 0;
|
public $offset = 0;
|
||||||
public $unread;
|
public $unread;
|
||||||
public $starred;
|
public $starred;
|
||||||
|
public $hidden;
|
||||||
public $labelled;
|
public $labelled;
|
||||||
public $annotated;
|
public $annotated;
|
||||||
|
|
||||||
|
@ -46,6 +47,10 @@ class Context extends ExclusionContext {
|
||||||
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
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) {
|
public function labelled(bool $spec = null) {
|
||||||
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
||||||
}
|
}
|
||||||
|
|
1068
lib/Database.php
1068
lib/Database.php
File diff suppressed because it is too large
Load diff
|
@ -74,6 +74,9 @@ interface Driver {
|
||||||
* - "greatest": the GREATEST function implemented by PostgreSQL and MySQL
|
* - "greatest": the GREATEST function implemented by PostgreSQL and MySQL
|
||||||
* - "nocase": the name of a general-purpose case-insensitive collation sequence
|
* - "nocase": the name of a general-purpose case-insensitive collation sequence
|
||||||
* - "like": the case-insensitive LIKE operator
|
* - "like": the case-insensitive LIKE operator
|
||||||
|
* - "integer": the integer type to use for explicit casts
|
||||||
|
* - "asc": ascending sort order when dealing with nulls
|
||||||
|
* - "desc": descending sort order when dealing with nulls
|
||||||
*/
|
*/
|
||||||
public function sqlToken(string $token): string;
|
public function sqlToken(string $token): string;
|
||||||
|
|
||||||
|
|
|
@ -81,6 +81,10 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
|
||||||
switch (strtolower($token)) {
|
switch (strtolower($token)) {
|
||||||
case "nocase":
|
case "nocase":
|
||||||
return '"utf8mb4_unicode_ci"';
|
return '"utf8mb4_unicode_ci"';
|
||||||
|
case "integer":
|
||||||
|
return "signed integer";
|
||||||
|
case "asc":
|
||||||
|
return "";
|
||||||
default:
|
default:
|
||||||
return $token;
|
return $token;
|
||||||
}
|
}
|
||||||
|
@ -160,6 +164,8 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
|
||||||
|
|
||||||
protected function makeConnection(string $db, string $user, string $password, string $host, int $port, string $socket): void {
|
protected function makeConnection(string $db, string $user, string $password, string $host, int $port, string $socket): void {
|
||||||
$this->db = mysqli_init();
|
$this->db = mysqli_init();
|
||||||
|
$this->db->options(\MYSQLI_SET_CHARSET_NAME, "utf8mb4");
|
||||||
|
$this->db->options(\MYSQLI_OPT_INT_AND_FLOAT_NATIVE, false);
|
||||||
$this->db->options(\MYSQLI_OPT_CONNECT_TIMEOUT, ceil(Arsse::$conf->dbTimeoutConnect));
|
$this->db->options(\MYSQLI_OPT_CONNECT_TIMEOUT, ceil(Arsse::$conf->dbTimeoutConnect));
|
||||||
@$this->db->real_connect($host, $user, $password, $db, $port, $socket);
|
@$this->db->real_connect($host, $user, $password, $db, $port, $socket);
|
||||||
if ($this->db->connect_errno) {
|
if ($this->db->connect_errno) {
|
||||||
|
@ -218,7 +224,7 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
|
||||||
// with MySQL each table must be analyzed separately, so we first have to get a list of tables
|
// with MySQL each table must be analyzed separately, so we first have to get a list of tables
|
||||||
foreach ($this->query("SHOW TABLES like 'arsse\\_%'") as $table) {
|
foreach ($this->query("SHOW TABLES like 'arsse\\_%'") as $table) {
|
||||||
$table = array_pop($table);
|
$table = array_pop($table);
|
||||||
if (!preg_match("/^arsse_[a-z_]+$/", $table)) {
|
if (!preg_match("/^arsse_[a-z_]+$/D", $table)) {
|
||||||
// table is not one of ours
|
// table is not one of ours
|
||||||
continue; // @codeCoverageIgnore
|
continue; // @codeCoverageIgnore
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,9 +16,9 @@ trait PDOError {
|
||||||
$err = $this->db->errorInfo();
|
$err = $this->db->errorInfo();
|
||||||
}
|
}
|
||||||
if ($err[0] === "HY000") {
|
if ($err[0] === "HY000") {
|
||||||
return static::buildEngineException($err[1], $err[2]);
|
return static::buildEngineException((string) $err[1], (string) $err[2]);
|
||||||
} else {
|
} else {
|
||||||
return static::buildStandardException($err[0], $err[2]);
|
return static::buildStandardException((string) $err[0], (string) $err[2]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -119,6 +119,10 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
|
||||||
return '"und-x-icu"';
|
return '"und-x-icu"';
|
||||||
case "like":
|
case "like":
|
||||||
return "ilike";
|
return "ilike";
|
||||||
|
case "asc":
|
||||||
|
return "nulls first";
|
||||||
|
case "desc":
|
||||||
|
return "desc nulls last";
|
||||||
default:
|
default:
|
||||||
return $token;
|
return $token;
|
||||||
}
|
}
|
||||||
|
|
26
lib/Db/PostgreSQL/PDOResult.php
Normal file
26
lib/Db/PostgreSQL/PDOResult.php
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<?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\Db\PostgreSQL;
|
||||||
|
|
||||||
|
class PDOResult extends \JKingWeb\Arsse\Db\PDOResult {
|
||||||
|
|
||||||
|
// This method exists to transparent handle byte-array results
|
||||||
|
|
||||||
|
public function valid() {
|
||||||
|
$this->cur = $this->set->fetch(\PDO::FETCH_ASSOC);
|
||||||
|
if ($this->cur !== false) {
|
||||||
|
foreach ($this->cur as $k => $v) {
|
||||||
|
if (is_resource($v)) {
|
||||||
|
$this->cur[$k] = stream_get_contents($v);
|
||||||
|
fclose($v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,8 @@
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse\Db\PostgreSQL;
|
namespace JKingWeb\Arsse\Db\PostgreSQL;
|
||||||
|
|
||||||
|
use JKingWeb\Arsse\Db\Result;
|
||||||
|
|
||||||
class PDOStatement extends \JKingWeb\Arsse\Db\PDOStatement {
|
class PDOStatement extends \JKingWeb\Arsse\Db\PDOStatement {
|
||||||
public static function mungeQuery(string $query, array $types, ...$extraData): string {
|
public static function mungeQuery(string $query, array $types, ...$extraData): string {
|
||||||
return Statement::mungeQuery($query, $types, false);
|
return Statement::mungeQuery($query, $types, false);
|
||||||
|
@ -16,4 +18,16 @@ class PDOStatement extends \JKingWeb\Arsse\Db\PDOStatement {
|
||||||
// PostgreSQL uses SQLSTATE exclusively, so this is not used
|
// PostgreSQL uses SQLSTATE exclusively, so this is not used
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function runArray(array $values = []): Result {
|
||||||
|
$this->st->closeCursor();
|
||||||
|
$this->bindValues($values);
|
||||||
|
try {
|
||||||
|
$this->st->execute();
|
||||||
|
} catch (\PDOException $e) {
|
||||||
|
[$excClass, $excMsg, $excData] = $this->buildPDOException(true);
|
||||||
|
throw new $excClass($excMsg, $excData);
|
||||||
|
}
|
||||||
|
return new PDOResult($this->db, $this->st);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ class Result extends \JKingWeb\Arsse\Db\AbstractResult {
|
||||||
protected $db;
|
protected $db;
|
||||||
protected $r;
|
protected $r;
|
||||||
protected $cur;
|
protected $cur;
|
||||||
|
protected $blobs = [];
|
||||||
|
|
||||||
// actual public methods
|
// actual public methods
|
||||||
|
|
||||||
|
@ -30,6 +31,11 @@ class Result extends \JKingWeb\Arsse\Db\AbstractResult {
|
||||||
public function __construct($db, $result) {
|
public function __construct($db, $result) {
|
||||||
$this->db = $db;
|
$this->db = $db;
|
||||||
$this->r = $result;
|
$this->r = $result;
|
||||||
|
for ($a = 0, $stop = pg_num_fields($result); $a < $stop; $a++) {
|
||||||
|
if (pg_field_type($result, $a) === "bytea") {
|
||||||
|
$this->blobs[$a] = pg_field_name($result, $a);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function __destruct() {
|
public function __destruct() {
|
||||||
|
@ -41,6 +47,14 @@ class Result extends \JKingWeb\Arsse\Db\AbstractResult {
|
||||||
|
|
||||||
public function valid() {
|
public function valid() {
|
||||||
$this->cur = pg_fetch_row($this->r, null, \PGSQL_ASSOC);
|
$this->cur = pg_fetch_row($this->r, null, \PGSQL_ASSOC);
|
||||||
return $this->cur !== false;
|
if ($this->cur !== false) {
|
||||||
|
foreach ($this->blobs as $f) {
|
||||||
|
if ($this->cur[$f]) {
|
||||||
|
$this->cur[$f] = hex2bin(substr($this->cur[$f], 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,6 +44,9 @@ class Statement extends \JKingWeb\Arsse\Db\AbstractStatement {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function bindValue($value, int $type, int $position): bool {
|
protected function bindValue($value, int $type, int $position): bool {
|
||||||
|
if ($value !== null && ($this->types[$position - 1] % self::T_NOT_NULL) === self::T_BINARY) {
|
||||||
|
$value = "\\x".bin2hex($value);
|
||||||
|
}
|
||||||
$this->in[] = $value;
|
$this->in[] = $value;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,12 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
|
||||||
$dbKey = Arsse::$conf->dbSQLite3Key;
|
$dbKey = Arsse::$conf->dbSQLite3Key;
|
||||||
$timeout = Arsse::$conf->dbSQLite3Timeout * 1000;
|
$timeout = Arsse::$conf->dbSQLite3Timeout * 1000;
|
||||||
try {
|
try {
|
||||||
|
// check whether the file exists; if it doesn't create the file and set its mode to rw-r-----
|
||||||
|
if ($dbFile !== ":memory:" && !file_exists($dbFile)) {
|
||||||
|
if (@touch($dbFile)) {
|
||||||
|
chmod($dbFile, 0640);
|
||||||
|
}
|
||||||
|
}
|
||||||
$this->makeConnection($dbFile, $dbKey);
|
$this->makeConnection($dbFile, $dbKey);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
// if opening the database doesn't work, check various pre-conditions to find out what the problem might be
|
// if opening the database doesn't work, check various pre-conditions to find out what the problem might be
|
||||||
|
@ -114,6 +120,8 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
|
||||||
switch (strtolower($token)) {
|
switch (strtolower($token)) {
|
||||||
case "greatest":
|
case "greatest":
|
||||||
return "max";
|
return "max";
|
||||||
|
case "asc":
|
||||||
|
return "";
|
||||||
default:
|
default:
|
||||||
return $token;
|
return $token;
|
||||||
}
|
}
|
||||||
|
|
13
lib/Factory.php
Normal file
13
lib/Factory.php
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<?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;
|
||||||
|
|
||||||
|
class Factory {
|
||||||
|
public function get(string $class) {
|
||||||
|
return new $class;
|
||||||
|
}
|
||||||
|
}
|
84
lib/Feed.php
84
lib/Feed.php
|
@ -7,6 +7,7 @@ declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse;
|
namespace JKingWeb\Arsse;
|
||||||
|
|
||||||
use JKingWeb\Arsse\Misc\Date;
|
use JKingWeb\Arsse\Misc\Date;
|
||||||
|
use JKingWeb\Arsse\Rule\Rule;
|
||||||
use PicoFeed\PicoFeedException;
|
use PicoFeed\PicoFeedException;
|
||||||
use PicoFeed\Config\Config;
|
use PicoFeed\Config\Config;
|
||||||
use PicoFeed\Client\Client;
|
use PicoFeed\Client\Client;
|
||||||
|
@ -16,13 +17,16 @@ use PicoFeed\Scraper\Scraper;
|
||||||
|
|
||||||
class Feed {
|
class Feed {
|
||||||
public $data = null;
|
public $data = null;
|
||||||
public $favicon;
|
public $iconUrl;
|
||||||
|
public $iconType;
|
||||||
|
public $iconData;
|
||||||
public $resource;
|
public $resource;
|
||||||
public $modified = false;
|
public $modified = false;
|
||||||
public $lastModified;
|
public $lastModified;
|
||||||
public $nextFetch;
|
public $nextFetch;
|
||||||
public $newItems = [];
|
public $newItems = [];
|
||||||
public $changedItems = [];
|
public $changedItems = [];
|
||||||
|
public $filteredItems = [];
|
||||||
|
|
||||||
public static function discover(string $url, string $username = '', string $password = ''): string {
|
public static function discover(string $url, string $username = '', string $password = ''): string {
|
||||||
// fetch the candidate feed
|
// fetch the candidate feed
|
||||||
|
@ -35,7 +39,7 @@ class Feed {
|
||||||
if (!$links) {
|
if (!$links) {
|
||||||
// work around a PicoFeed memory leak
|
// work around a PicoFeed memory leak
|
||||||
libxml_use_internal_errors(false);
|
libxml_use_internal_errors(false);
|
||||||
throw new Feed\Exception($url, new \PicoFeed\Reader\SubscriptionNotFoundException('Unable to find a subscription'));
|
throw new Feed\Exception("", ['url' => $url], new \PicoFeed\Reader\SubscriptionNotFoundException('Unable to find a subscription'));
|
||||||
} else {
|
} else {
|
||||||
$out = $links[0];
|
$out = $links[0];
|
||||||
}
|
}
|
||||||
|
@ -45,6 +49,17 @@ class Feed {
|
||||||
return $out;
|
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())) {
|
||||||
|
// if the prospective URL is a feed, use it
|
||||||
|
return [$url];
|
||||||
|
} else {
|
||||||
|
return $f->reader->find($f->getUrl(), $f->getContent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function __construct(int $feedID = null, string $url, string $lastModified = '', string $etag = '', string $username = '', string $password = '', bool $scrape = false) {
|
public function __construct(int $feedID = null, string $url, string $lastModified = '', string $etag = '', string $username = '', string $password = '', bool $scrape = false) {
|
||||||
// fetch the feed
|
// fetch the feed
|
||||||
$this->resource = self::download($url, $lastModified, $etag, $username, $password);
|
$this->resource = self::download($url, $lastModified, $etag, $username, $password);
|
||||||
|
@ -66,10 +81,14 @@ class Feed {
|
||||||
// we only really care if articles have been modified; if there are no new articles, act as if the feed is unchanged
|
// we only really care if articles have been modified; if there are no new articles, act as if the feed is unchanged
|
||||||
if (!sizeof($this->newItems) && !sizeof($this->changedItems)) {
|
if (!sizeof($this->newItems) && !sizeof($this->changedItems)) {
|
||||||
$this->modified = false;
|
$this->modified = false;
|
||||||
}
|
} else {
|
||||||
// if requested, scrape full content for any new and changed items
|
if ($feedID) {
|
||||||
if ($scrape) {
|
$this->computeFilterRules($feedID);
|
||||||
$this->scrape();
|
}
|
||||||
|
// if requested, scrape full content for any new and changed items
|
||||||
|
if ($scrape) {
|
||||||
|
$this->scrape();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// compute the time at which the feed should next be fetched
|
// compute the time at which the feed should next be fetched
|
||||||
|
@ -100,27 +119,35 @@ class Feed {
|
||||||
$client->reader = $reader;
|
$client->reader = $reader;
|
||||||
return $client;
|
return $client;
|
||||||
} catch (PicoFeedException $e) {
|
} catch (PicoFeedException $e) {
|
||||||
throw new Feed\Exception($url, $e); // @codeCoverageIgnore
|
throw new Feed\Exception("", ['url' => $url], $e); // @codeCoverageIgnore
|
||||||
} catch (\GuzzleHttp\Exception\GuzzleException $e) {
|
} catch (\GuzzleHttp\Exception\GuzzleException $e) {
|
||||||
throw new Feed\Exception($url, $e);
|
throw new Feed\Exception("", ['url' => $url], $e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function parse(): bool {
|
protected function parse(): void {
|
||||||
try {
|
try {
|
||||||
$feed = $this->resource->reader->getParser(
|
$feed = $this->resource->reader->getParser(
|
||||||
$this->resource->getUrl(),
|
$this->resource->getUrl(),
|
||||||
$this->resource->getContent(),
|
$this->resource->getContent(),
|
||||||
$this->resource->getEncoding()
|
$this->resource->getEncoding()
|
||||||
)->execute();
|
)->execute();
|
||||||
// Grab the favicon for the feed; returns an empty string if it cannot find one.
|
|
||||||
// Some feeds might use a different domain (eg: feedburner), so the site url is
|
|
||||||
// used instead of the feed's url.
|
|
||||||
$this->favicon = (new Favicon)->find($feed->siteUrl);
|
|
||||||
} catch (PicoFeedException $e) {
|
} catch (PicoFeedException $e) {
|
||||||
throw new Feed\Exception($this->resource->getUrl(), $e);
|
throw new Feed\Exception("", ['url' => $this->resource->getUrl()], $e);
|
||||||
} catch (\GuzzleHttp\Exception\GuzzleException $e) { // @codeCoverageIgnore
|
} catch (\GuzzleHttp\Exception\GuzzleException $e) { // @codeCoverageIgnore
|
||||||
throw new Feed\Exception($this->resource->getUrl(), $e); // @codeCoverageIgnore
|
throw new Feed\Exception("", ['url' => $this->resource->getUrl()], $e); // @codeCoverageIgnore
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grab the favicon for the feed, or null if no valid icon is found
|
||||||
|
// Some feeds might use a different domain (eg: feedburner), so the site url is
|
||||||
|
// used instead of the feed's url.
|
||||||
|
$icon = new Favicon;
|
||||||
|
$this->iconUrl = $icon->find($feed->siteUrl, $feed->getIcon());
|
||||||
|
$this->iconData = $icon->getContent();
|
||||||
|
if (strlen($this->iconData)) {
|
||||||
|
$this->iconType = $icon->getType();
|
||||||
|
} else {
|
||||||
|
$this->iconUrl = $this->iconData = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// PicoFeed does not provide valid ids when there is no id element. Its solution
|
// PicoFeed does not provide valid ids when there is no id element. Its solution
|
||||||
|
@ -201,7 +228,6 @@ class Feed {
|
||||||
sort($f->categories);
|
sort($f->categories);
|
||||||
}
|
}
|
||||||
$this->data = $feed;
|
$this->data = $feed;
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function deduplicateItems(array $items): array {
|
protected function deduplicateItems(array $items): array {
|
||||||
|
@ -248,19 +274,19 @@ class Feed {
|
||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function matchToDatabase(int $feedID = null): bool {
|
protected function matchToDatabase(int $feedID = null): void {
|
||||||
// first perform deduplication on items
|
// first perform deduplication on items
|
||||||
$items = $this->deduplicateItems($this->data->items);
|
$items = $this->deduplicateItems($this->data->items);
|
||||||
// if we haven't been given a database feed ID to check against, all items are new
|
// if we haven't been given a database feed ID to check against, all items are new
|
||||||
if (is_null($feedID)) {
|
if (is_null($feedID)) {
|
||||||
$this->newItems = $items;
|
$this->newItems = $items;
|
||||||
return true;
|
return;
|
||||||
}
|
}
|
||||||
// get as many of the latest articles in the database as there are in the feed
|
// get as many of the latest articles in the database as there are in the feed
|
||||||
$articles = Arsse::$db->feedMatchLatest($feedID, sizeof($items))->getAll();
|
$articles = Arsse::$db->feedMatchLatest($feedID, sizeof($items))->getAll();
|
||||||
// perform a first pass matching the latest articles against items in the feed
|
// perform a first pass matching the latest articles against items in the feed
|
||||||
[$this->newItems, $this->changedItems] = $this->matchItems($items, $articles);
|
[$this->newItems, $this->changedItems] = $this->matchItems($items, $articles);
|
||||||
if (sizeof($this->newItems) && sizeof($items) <= sizeof($articles)) {
|
if (sizeof($this->newItems)) {
|
||||||
// if we need to, perform a second pass on the database looking specifically for IDs and hashes of the new items
|
// if we need to, perform a second pass on the database looking specifically for IDs and hashes of the new items
|
||||||
$ids = $hashesUT = $hashesUC = $hashesTC = [];
|
$ids = $hashesUT = $hashesUC = $hashesTC = [];
|
||||||
foreach ($this->newItems as $i) {
|
foreach ($this->newItems as $i) {
|
||||||
|
@ -282,7 +308,6 @@ class Feed {
|
||||||
// merge the two change-lists, preserving keys
|
// merge the two change-lists, preserving keys
|
||||||
$this->changedItems = array_combine(array_merge(array_keys($this->changedItems), array_keys($changed)), array_merge($this->changedItems, $changed));
|
$this->changedItems = array_combine(array_merge(array_keys($this->changedItems), array_keys($changed)), array_merge($this->changedItems, $changed));
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function matchItems(array $items, array $articles): array {
|
protected function matchItems(array $items, array $articles): array {
|
||||||
|
@ -417,15 +442,28 @@ class Feed {
|
||||||
return $dates;
|
return $dates;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function scrape(): bool {
|
protected function scrape(): void {
|
||||||
$scraper = new Scraper(self::configure());
|
$scraper = new Scraper(self::configure());
|
||||||
foreach (array_merge($this->newItems, $this->changedItems) as $item) {
|
foreach (array_merge($this->newItems, $this->changedItems) as $item) {
|
||||||
$scraper->setUrl($item->url);
|
$scraper->setUrl($item->url);
|
||||||
$scraper->execute();
|
$scraper->execute();
|
||||||
if ($scraper->hasRelevantContent()) {
|
if ($scraper->hasRelevantContent()) {
|
||||||
$item->content = $scraper->getFilteredContent();
|
$item->scrapedContent = $scraper->getFilteredContent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
}
|
||||||
|
|
||||||
|
protected function computeFilterRules(int $feedID): void {
|
||||||
|
$rules = Arsse::$db->feedRulesGet($feedID);
|
||||||
|
foreach ($rules as $user => $r) {
|
||||||
|
$stats = ['new' => [], 'changed' => []];
|
||||||
|
foreach ($this->newItems as $index => $item) {
|
||||||
|
$stats['new'][$index] = Rule::apply($r['keep'], $r['block'], $item->title, $item->categories);
|
||||||
|
}
|
||||||
|
foreach ($this->changedItems as $index => $item) {
|
||||||
|
$stats['changed'][$index] = Rule::apply($r['keep'], $r['block'], $item->title, $item->categories);
|
||||||
|
}
|
||||||
|
$this->filteredItems[$user] = $stats;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,30 +15,33 @@ class Exception extends \JKingWeb\Arsse\AbstractException {
|
||||||
protected const CURL_ERROR_MAP = [1 => "invalidUrl",3 => "invalidUrl",5 => "transmissionError","connectionFailed","connectionFailed","transmissionError","forbidden","unauthorized","transmissionError","transmissionError","transmissionError","transmissionError","connectionFailed","connectionFailed","transmissionError","transmissionError","transmissionError","transmissionError","transmissionError","invalidUrl","transmissionError","transmissionError","transmissionError","transmissionError",28 => "timeout","transmissionError","transmissionError","transmissionError","transmissionError","transmissionError",35 => "invalidCertificate","transmissionError","transmissionError","transmissionError","transmissionError",45 => "transmissionError","unauthorized","maxRedirect",52 => "transmissionError","invalidCertificate","invalidCertificate","transmissionError","transmissionError",58 => "invalidCertificate","invalidCertificate","invalidCertificate","transmissionError","invalidUrl","transmissionError","invalidCertificate","transmissionError","invalidCertificate","forbidden","invalidUrl","forbidden","transmissionError",73 => "transmissionError","transmissionError",77 => "invalidCertificate","invalidUrl",90 => "invalidCertificate","invalidCertificate","transmissionError",94 => "unauthorized","transmissionError","connectionFailed"];
|
protected const CURL_ERROR_MAP = [1 => "invalidUrl",3 => "invalidUrl",5 => "transmissionError","connectionFailed","connectionFailed","transmissionError","forbidden","unauthorized","transmissionError","transmissionError","transmissionError","transmissionError","connectionFailed","connectionFailed","transmissionError","transmissionError","transmissionError","transmissionError","transmissionError","invalidUrl","transmissionError","transmissionError","transmissionError","transmissionError",28 => "timeout","transmissionError","transmissionError","transmissionError","transmissionError","transmissionError",35 => "invalidCertificate","transmissionError","transmissionError","transmissionError","transmissionError",45 => "transmissionError","unauthorized","maxRedirect",52 => "transmissionError","invalidCertificate","invalidCertificate","transmissionError","transmissionError",58 => "invalidCertificate","invalidCertificate","invalidCertificate","transmissionError","invalidUrl","transmissionError","invalidCertificate","transmissionError","invalidCertificate","forbidden","invalidUrl","forbidden","transmissionError",73 => "transmissionError","transmissionError",77 => "invalidCertificate","invalidUrl",90 => "invalidCertificate","invalidCertificate","transmissionError",94 => "unauthorized","transmissionError","connectionFailed"];
|
||||||
protected const HTTP_ERROR_MAP = [401 => "unauthorized",403 => "forbidden",404 => "invalidUrl",408 => "timeout",410 => "invalidUrl",414 => "invalidUrl",451 => "invalidUrl"];
|
protected const HTTP_ERROR_MAP = [401 => "unauthorized",403 => "forbidden",404 => "invalidUrl",408 => "timeout",410 => "invalidUrl",414 => "invalidUrl",451 => "invalidUrl"];
|
||||||
|
|
||||||
public function __construct($url, \Throwable $e) {
|
public function __construct(string $msgID = "", $vars = null, \Throwable $e = null) {
|
||||||
if ($e instanceof BadResponseException) {
|
if ($msgID === "") {
|
||||||
$msgID = self::HTTP_ERROR_MAP[$e->getCode()] ?? "transmissionError";
|
assert($e !== null, new \Exception("Expecting Picofeed or Guzzle exception when no message specified."));
|
||||||
} elseif ($e instanceof TooManyRedirectsException) {
|
if ($e instanceof BadResponseException) {
|
||||||
$msgID = "maxRedirect";
|
$msgID = self::HTTP_ERROR_MAP[$e->getCode()] ?? "transmissionError";
|
||||||
} elseif ($e instanceof GuzzleException) {
|
} elseif ($e instanceof TooManyRedirectsException) {
|
||||||
$msg = $e->getMessage();
|
$msgID = "maxRedirect";
|
||||||
if (preg_match("/^Error creating resource:/", $msg)) {
|
} elseif ($e instanceof GuzzleException) {
|
||||||
// PHP stream error; the class of error is ambiguous
|
$msg = $e->getMessage();
|
||||||
$msgID = "transmissionError";
|
if (preg_match("/^Error creating resource:/", $msg)) {
|
||||||
} elseif (preg_match("/^cURL error (\d+):/", $msg, $match)) {
|
// PHP stream error; the class of error is ambiguous
|
||||||
$msgID = self::CURL_ERROR_MAP[(int) $match[1]] ?? "internalError";
|
$msgID = "transmissionError";
|
||||||
|
} elseif (preg_match("/^cURL error (\d+):/", $msg, $match)) {
|
||||||
|
$msgID = self::CURL_ERROR_MAP[(int) $match[1]] ?? "internalError";
|
||||||
|
} else {
|
||||||
|
$msgID = "internalError";
|
||||||
|
}
|
||||||
|
} elseif ($e instanceof PicoFeedException) {
|
||||||
|
$className = get_class($e);
|
||||||
|
// Convert the exception thrown by PicoFeed to the one to be thrown here.
|
||||||
|
$msgID = preg_replace('/^PicoFeed\\\(?:Client|Parser|Reader)\\\([A-Za-z]+)Exception$/', '$1', $className);
|
||||||
|
// If the message ID doesn't change then it's unknown.
|
||||||
|
$msgID = ($msgID !== $className) ? lcfirst($msgID) : "internalError";
|
||||||
} else {
|
} else {
|
||||||
$msgID = "internalError";
|
$msgID = "internalError";
|
||||||
}
|
}
|
||||||
} elseif ($e instanceof PicoFeedException) {
|
|
||||||
$className = get_class($e);
|
|
||||||
// Convert the exception thrown by PicoFeed to the one to be thrown here.
|
|
||||||
$msgID = preg_replace('/^PicoFeed\\\(?:Client|Parser|Reader)\\\([A-Za-z]+)Exception$/', '$1', $className);
|
|
||||||
// If the message ID doesn't change then it's unknown.
|
|
||||||
$msgID = ($msgID !== $className) ? lcfirst($msgID) : "internalError";
|
|
||||||
} else {
|
|
||||||
$msgID = "internalError";
|
|
||||||
}
|
}
|
||||||
parent::__construct($msgID, ['url' => $url], $e);
|
parent::__construct($msgID, $vars, $e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,11 +9,11 @@ namespace JKingWeb\Arsse\ImportExport;
|
||||||
use JKingWeb\Arsse\Arsse;
|
use JKingWeb\Arsse\Arsse;
|
||||||
use JKingWeb\Arsse\Database;
|
use JKingWeb\Arsse\Database;
|
||||||
use JKingWeb\Arsse\Db\ExceptionInput as InputException;
|
use JKingWeb\Arsse\Db\ExceptionInput as InputException;
|
||||||
use JKingWeb\Arsse\User\Exception as UserException;
|
use JKingWeb\Arsse\User\ExceptionConflict as UserException;
|
||||||
|
|
||||||
abstract class AbstractImportExport {
|
abstract class AbstractImportExport {
|
||||||
public function import(string $user, string $data, bool $flat = false, bool $replace = false): bool {
|
public function import(string $user, string $data, bool $flat = false, bool $replace = false): bool {
|
||||||
if (!Arsse::$user->exists($user)) {
|
if (!Arsse::$db->userExists($user)) {
|
||||||
throw new UserException("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
|
throw new UserException("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
|
||||||
}
|
}
|
||||||
// first extract useful information from the input
|
// first extract useful information from the input
|
||||||
|
|
|
@ -7,7 +7,7 @@ declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse\ImportExport;
|
namespace JKingWeb\Arsse\ImportExport;
|
||||||
|
|
||||||
use JKingWeb\Arsse\Arsse;
|
use JKingWeb\Arsse\Arsse;
|
||||||
use JKingWeb\Arsse\User\Exception as UserException;
|
use JKingWeb\Arsse\User\ExceptionConflict as UserException;
|
||||||
|
|
||||||
class OPML extends AbstractImportExport {
|
class OPML extends AbstractImportExport {
|
||||||
protected function parse(string $opml, bool $flat): array {
|
protected function parse(string $opml, bool $flat): array {
|
||||||
|
@ -91,7 +91,7 @@ class OPML extends AbstractImportExport {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function export(string $user, bool $flat = false): string {
|
public function export(string $user, bool $flat = false): string {
|
||||||
if (!Arsse::$user->exists($user)) {
|
if (!Arsse::$db->userExists($user)) {
|
||||||
throw new UserException("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
|
throw new UserException("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
|
||||||
}
|
}
|
||||||
$tags = [];
|
$tags = [];
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse\Misc;
|
namespace JKingWeb\Arsse\Misc;
|
||||||
|
|
||||||
class Date {
|
abstract class Date {
|
||||||
public static function transform($date, string $outFormat = null, string $inFormat = null) {
|
public static function transform($date, string $outFormat = null, string $inFormat = null) {
|
||||||
$date = ValueInfo::normalize($date, ValueInfo::T_DATE, $inFormat);
|
$date = ValueInfo::normalize($date, ValueInfo::T_DATE, $inFormat);
|
||||||
if (!$date) {
|
if (!$date) {
|
||||||
|
|
|
@ -12,7 +12,7 @@ class HTTP {
|
||||||
public static function matchType(MessageInterface $msg, string ...$type): bool {
|
public static function matchType(MessageInterface $msg, string ...$type): bool {
|
||||||
$header = $msg->getHeaderLine("Content-Type") ?? "";
|
$header = $msg->getHeaderLine("Content-Type") ?? "";
|
||||||
foreach ($type as $t) {
|
foreach ($type as $t) {
|
||||||
$pattern = "/^".preg_quote(trim($t), "/")."\s*($|;|,)/i";
|
$pattern = "/^".preg_quote(trim($t), "/")."\s*($|;|,)/Di";
|
||||||
if (preg_match($pattern, $header)) {
|
if (preg_match($pattern, $header)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,7 +85,7 @@ class URL {
|
||||||
if ($c === "%") {
|
if ($c === "%") {
|
||||||
// the % character signals an encoded character...
|
// the % character signals an encoded character...
|
||||||
$d = substr($part, $pos + 1, 2);
|
$d = substr($part, $pos + 1, 2);
|
||||||
if (!preg_match("/^[0-9a-fA-F]{2}$/", $d)) {
|
if (!preg_match("/^[0-9a-fA-F]{2}$/D", $d)) {
|
||||||
// unless there are fewer than two characters left in the string or the two characters are not hex digits
|
// unless there are fewer than two characters left in the string or the two characters are not hex digits
|
||||||
$d = ord($c);
|
$d = ord($c);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -35,6 +35,17 @@ class ValueInfo {
|
||||||
public const M_DROP = 1 << 29; // drop the value (return null) if the type doesn't match
|
public const M_DROP = 1 << 29; // drop the value (return null) if the type doesn't match
|
||||||
public const M_STRICT = 1 << 30; // throw an exception if the type doesn't match
|
public const M_STRICT = 1 << 30; // throw an exception if the type doesn't match
|
||||||
public const M_ARRAY = 1 << 31; // the value should be a flat array of values of the specified type; indexed and associative are both acceptable
|
public const M_ARRAY = 1 << 31; // the value should be a flat array of values of the specified type; indexed and associative are both acceptable
|
||||||
|
public const TYPE_NAMES = [
|
||||||
|
self::T_MIXED => "mixed",
|
||||||
|
self::T_NULL => "null",
|
||||||
|
self::T_BOOL => "boolean",
|
||||||
|
self::T_INT => "integer",
|
||||||
|
self::T_FLOAT => "float",
|
||||||
|
self::T_DATE => "date",
|
||||||
|
self::T_STRING => "string",
|
||||||
|
self::T_ARRAY => "array",
|
||||||
|
self::T_INTERVAL => "interval",
|
||||||
|
];
|
||||||
// symbolic date and time formats
|
// symbolic date and time formats
|
||||||
protected const DATE_FORMATS = [ // in out
|
protected const DATE_FORMATS = [ // in out
|
||||||
'iso8601' => ["!Y-m-d\TH:i:s", "Y-m-d\TH:i:s\Z" ], // NOTE: ISO 8601 dates require special input processing because of varying formats for timezone offsets
|
'iso8601' => ["!Y-m-d\TH:i:s", "Y-m-d\TH:i:s\Z" ], // NOTE: ISO 8601 dates require special input processing because of varying formats for timezone offsets
|
||||||
|
@ -211,7 +222,7 @@ class ValueInfo {
|
||||||
return $out;
|
return $out;
|
||||||
} else {
|
} else {
|
||||||
$out = sprintf("%F", $value);
|
$out = sprintf("%F", $value);
|
||||||
return preg_match("/\.0{1,}$/", $out) ? (string) (int) $out : $out;
|
return preg_match("/\.0{1,}$/D", $out) ? (string) (int) $out : $out;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$info = self::str($value);
|
$info = self::str($value);
|
||||||
|
@ -245,7 +256,7 @@ class ValueInfo {
|
||||||
$out = false;
|
$out = false;
|
||||||
if ($dateInFormat === "microtime") {
|
if ($dateInFormat === "microtime") {
|
||||||
// PHP is not able to correctly handle the output of microtime() as the input of DateTime::createFromFormat(), so we fudge it to look like a float
|
// PHP is not able to correctly handle the output of microtime() as the input of DateTime::createFromFormat(), so we fudge it to look like a float
|
||||||
if (preg_match("<^0\.\d{6}00 \d+$>", $value)) {
|
if (preg_match("<^0\.\d{6}00 \d+$>D", $value)) {
|
||||||
$value = substr($value, 11).".".substr($value, 2, 6);
|
$value = substr($value, 11).".".substr($value, 2, 6);
|
||||||
} else {
|
} else {
|
||||||
throw new \Exception;
|
throw new \Exception;
|
||||||
|
|
39
lib/REST.php
39
lib/REST.php
|
@ -51,6 +51,21 @@ class REST {
|
||||||
'strip' => "/u/",
|
'strip' => "/u/",
|
||||||
'class' => REST\Microsub\Auth::class,
|
'class' => REST\Microsub\Auth::class,
|
||||||
],
|
],
|
||||||
|
'miniflux' => [ // Miniflux https://miniflux.app/docs/api.html
|
||||||
|
'match' => '/v1/',
|
||||||
|
'strip' => '/v1',
|
||||||
|
'class' => REST\Miniflux\V1::class,
|
||||||
|
],
|
||||||
|
'miniflux-version' => [ // Miniflux version report
|
||||||
|
'match' => '/version',
|
||||||
|
'strip' => '',
|
||||||
|
'class' => REST\Miniflux\Status::class,
|
||||||
|
],
|
||||||
|
'miniflux-healthcheck' => [ // Miniflux health check
|
||||||
|
'match' => '/healthcheck',
|
||||||
|
'strip' => '',
|
||||||
|
'class' => REST\Miniflux\Status::class,
|
||||||
|
],
|
||||||
// Other candidates:
|
// Other candidates:
|
||||||
// Google Reader http://feedhq.readthedocs.io/en/latest/api/index.html
|
// Google Reader http://feedhq.readthedocs.io/en/latest/api/index.html
|
||||||
// Feedbin v2 https://github.com/feedbin/feedbin-api
|
// Feedbin v2 https://github.com/feedbin/feedbin-api
|
||||||
|
@ -58,7 +73,6 @@ class REST {
|
||||||
// Selfoss https://github.com/SSilence/selfoss/wiki/Restful-API-for-Apps-or-any-other-external-access
|
// Selfoss https://github.com/SSilence/selfoss/wiki/Restful-API-for-Apps-or-any-other-external-access
|
||||||
// NewsBlur http://www.newsblur.com/api
|
// NewsBlur http://www.newsblur.com/api
|
||||||
// Unclear if clients exist:
|
// Unclear if clients exist:
|
||||||
// Miniflux https://docs.miniflux.app/en/latest/api.html#api-reference
|
|
||||||
// Nextcloud News v2 https://github.com/nextcloud/news/blob/master/docs/externalapi/External-Api.md
|
// Nextcloud News v2 https://github.com/nextcloud/news/blob/master/docs/externalapi/External-Api.md
|
||||||
// BirdReader https://github.com/glynnbird/birdreader/blob/master/API.md
|
// BirdReader https://github.com/glynnbird/birdreader/blob/master/API.md
|
||||||
// Feedbin v1 https://github.com/feedbin/feedbin-api/commit/86da10aac5f1a57531a6e17b08744e5f9e7db8a9
|
// Feedbin v1 https://github.com/feedbin/feedbin-api/commit/86da10aac5f1a57531a6e17b08744e5f9e7db8a9
|
||||||
|
@ -76,17 +90,19 @@ class REST {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function dispatch(ServerRequestInterface $req = null): ResponseInterface {
|
public function dispatch(ServerRequestInterface $req = null): ResponseInterface {
|
||||||
// create a request object if not provided
|
|
||||||
$req = $req ?? ServerRequestFactory::fromGlobals();
|
|
||||||
// find the API to handle
|
|
||||||
try {
|
try {
|
||||||
[$api, $target, $class] = $this->apiMatch($req->getRequestTarget(), $this->apis);
|
// ensure the require extensions are loaded
|
||||||
|
Arsse::checkExtensions(...Arsse::REQUIRED_EXTENSIONS);
|
||||||
|
// create a request object if not provided
|
||||||
|
$req = $req ?? ServerRequestFactory::fromGlobals();
|
||||||
|
// find the API to handle
|
||||||
|
[, $target, $class] = $this->apiMatch($req->getRequestTarget(), $this->apis);
|
||||||
// authenticate the request pre-emptively
|
// authenticate the request pre-emptively
|
||||||
$req = $this->authenticateRequest($req);
|
$req = $this->authenticateRequest($req);
|
||||||
// modify the request to have an uppercase method and a stripped target
|
// modify the request to have an uppercase method and a stripped target
|
||||||
$req = $req->withMethod(strtoupper($req->getMethod()))->withRequestTarget($target);
|
$req = $req->withMethod(strtoupper($req->getMethod()))->withRequestTarget($target);
|
||||||
// fetch the correct handler
|
// fetch the correct handler
|
||||||
$drv = $this->getHandler($class);
|
$drv = Arsse::$obj->get($class);
|
||||||
// generate a response
|
// generate a response
|
||||||
if ($req->getMethod() === "HEAD") {
|
if ($req->getMethod() === "HEAD") {
|
||||||
// if the request is a HEAD request, we act exactly as if it were a GET request, and simply remove the response body later
|
// if the request is a HEAD request, we act exactly as if it were a GET request, and simply remove the response body later
|
||||||
|
@ -101,11 +117,6 @@ class REST {
|
||||||
return $this->normalizeResponse($res, $req);
|
return $this->normalizeResponse($res, $req);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getHandler(string $className): REST\Handler {
|
|
||||||
// instantiate the API handler
|
|
||||||
return new $className();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function apiMatch(string $url): array {
|
public function apiMatch(string $url): array {
|
||||||
$map = $this->apis;
|
$map = $this->apis;
|
||||||
// sort the API list so the longest URL prefixes come first
|
// sort the API list so the longest URL prefixes come first
|
||||||
|
@ -119,7 +130,7 @@ class REST {
|
||||||
// first try a simple substring match
|
// first try a simple substring match
|
||||||
if (strpos($url, $api['match']) === 0) {
|
if (strpos($url, $api['match']) === 0) {
|
||||||
// if it matches, perform a more rigorous match and then strip off any defined prefix
|
// if it matches, perform a more rigorous match and then strip off any defined prefix
|
||||||
$pattern = "<^".preg_quote($api['match'])."([/\?#]|$)>";
|
$pattern = "<^".preg_quote($api['match'])."([/\?#]|$)>D";
|
||||||
if ($url === $api['match'] || in_array(substr($api['match'], -1, 1), ["/", "?", "#"]) || preg_match($pattern, $url)) {
|
if ($url === $api['match'] || in_array(substr($api['match'], -1, 1), ["/", "?", "#"]) || preg_match($pattern, $url)) {
|
||||||
$target = substr($url, strlen($api['strip']));
|
$target = substr($url, strlen($api['strip']));
|
||||||
} else {
|
} else {
|
||||||
|
@ -262,7 +273,7 @@ class REST {
|
||||||
// if the origin is the special value "null", use it
|
// if the origin is the special value "null", use it
|
||||||
return "null";
|
return "null";
|
||||||
}
|
}
|
||||||
if (preg_match("<^([^:]+)://(\[[^\]]+\]|[^\[\]:/\?#@]+)((?::.*)?)$>i", $origin, $match)) {
|
if (preg_match("<^([^:]+)://(\[[^\]]+\]|[^\[\]:/\?#@]+)((?::.*)?)$>Di", $origin, $match)) {
|
||||||
// if the origin sort-of matches the syntax in a general sense, continue
|
// if the origin sort-of matches the syntax in a general sense, continue
|
||||||
$scheme = $match[1];
|
$scheme = $match[1];
|
||||||
$host = $match[2];
|
$host = $match[2];
|
||||||
|
@ -270,7 +281,7 @@ class REST {
|
||||||
// decode and normalize the scheme and port (the port may be blank)
|
// decode and normalize the scheme and port (the port may be blank)
|
||||||
$scheme = strtolower(rawurldecode($scheme));
|
$scheme = strtolower(rawurldecode($scheme));
|
||||||
$port = rawurldecode($port);
|
$port = rawurldecode($port);
|
||||||
if (!preg_match("<^(?::[0-9]+)?$>", $port) || !preg_match("<^[a-z](?:[a-z0-9\+\-\.])*$>", $scheme)) {
|
if (!preg_match("<^(?::[0-9]+)?$>D", $port) || !preg_match("<^[a-z](?:[a-z0-9\+\-\.])*$>D", $scheme)) {
|
||||||
// if the normalized port contains anything but numbers, or the scheme does not follow the generic URL syntax, the origin is invalid
|
// if the normalized port contains anything but numbers, or the scheme does not follow the generic URL syntax, the origin is invalid
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,8 @@
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse\REST;
|
namespace JKingWeb\Arsse\REST;
|
||||||
|
|
||||||
|
use JKingWeb\Arsse\Arsse;
|
||||||
use JKingWeb\Arsse\Misc\Date;
|
use JKingWeb\Arsse\Misc\Date;
|
||||||
use JKingWeb\Arsse\Misc\ValueInfo;
|
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
|
||||||
|
@ -15,6 +15,14 @@ abstract class AbstractHandler implements Handler {
|
||||||
abstract public function __construct();
|
abstract public function __construct();
|
||||||
abstract public function dispatch(ServerRequestInterface $req): ResponseInterface;
|
abstract public function dispatch(ServerRequestInterface $req): ResponseInterface;
|
||||||
|
|
||||||
|
protected function now(): \DateTimeImmutable {
|
||||||
|
return Arsse::$obj->get(\DateTimeImmutable::class)->setTimezone(new \DateTimeZone("UTC"));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function isAdmin(): bool {
|
||||||
|
return (bool) Arsse::$user->propertiesGet(Arsse::$user->id, false)['admin'];
|
||||||
|
}
|
||||||
|
|
||||||
protected function fieldMapNames(array $data, array $map): array {
|
protected function fieldMapNames(array $data, array $map): array {
|
||||||
$out = [];
|
$out = [];
|
||||||
foreach ($map as $to => $from) {
|
foreach ($map as $to => $from) {
|
||||||
|
@ -37,16 +45,4 @@ abstract class AbstractHandler implements Handler {
|
||||||
}
|
}
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function normalizeInput(array $data, array $types, string $dateFormat = null, int $mode = 0): array {
|
|
||||||
$out = [];
|
|
||||||
foreach ($types as $key => $type) {
|
|
||||||
if (isset($data[$key])) {
|
|
||||||
$out[$key] = ValueInfo::normalize($data[$key], $type | $mode, $dateFormat);
|
|
||||||
} else {
|
|
||||||
$out[$key] = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $out;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,9 +72,6 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
]);
|
]);
|
||||||
case "GET": // HTTP violation required for client "Unread" on iOS
|
case "GET": // HTTP violation required for client "Unread" on iOS
|
||||||
case "POST":
|
case "POST":
|
||||||
if (!HTTP::matchType($req, "", ...self::ACCEPTED_TYPES)) {
|
|
||||||
return new EmptyResponse(415, ['Accept' => implode(", ", self::ACCEPTED_TYPES)]);
|
|
||||||
}
|
|
||||||
$out = [
|
$out = [
|
||||||
'api_version' => self::LEVEL,
|
'api_version' => self::LEVEL,
|
||||||
'auth' => 0,
|
'auth' => 0,
|
||||||
|
@ -150,28 +147,21 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
$out['feeds_groups'] = $this->getRelationships();
|
$out['feeds_groups'] = $this->getRelationships();
|
||||||
}
|
}
|
||||||
if ($G['favicons']) {
|
if ($G['favicons']) {
|
||||||
// TODO: implement favicons properly
|
$out['favicons'] = $this->getIcons();
|
||||||
// we provide a single blank favicon for now
|
|
||||||
$out['favicons'] = [
|
|
||||||
[
|
|
||||||
'id' => 0,
|
|
||||||
'data' => self::GENERIC_ICON_TYPE.",".self::GENERIC_ICON_DATA,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
if ($G['items']) {
|
if ($G['items']) {
|
||||||
$out['items'] = $this->getItems($G);
|
$out['items'] = $this->getItems($G);
|
||||||
$out['total_items'] = Arsse::$db->articleCount(Arsse::$user->id);
|
$out['total_items'] = Arsse::$db->articleCount(Arsse::$user->id, (new Context)->hidden(false));
|
||||||
}
|
}
|
||||||
if ($G['links']) {
|
if ($G['links']) {
|
||||||
// TODO: implement hot links
|
// TODO: implement hot links
|
||||||
$out['links'] = [];
|
$out['links'] = [];
|
||||||
}
|
}
|
||||||
if ($G['unread_item_ids'] || $listUnread) {
|
if ($G['unread_item_ids'] || $listUnread) {
|
||||||
$out['unread_item_ids'] = $this->getItemIds((new Context)->unread(true));
|
$out['unread_item_ids'] = $this->getItemIds((new Context)->unread(true)->hidden(false));
|
||||||
}
|
}
|
||||||
if ($G['saved_item_ids'] || $listSaved) {
|
if ($G['saved_item_ids'] || $listSaved) {
|
||||||
$out['saved_item_ids'] = $this->getItemIds((new Context)->starred(true));
|
$out['saved_item_ids'] = $this->getItemIds((new Context)->starred(true)->hidden(false));
|
||||||
}
|
}
|
||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
|
@ -241,7 +231,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
try {
|
try {
|
||||||
// verify the supplied hash is valid
|
// verify the supplied hash is valid
|
||||||
$s = Arsse::$db->TokenLookup("fever.login", $hash);
|
$s = Arsse::$db->TokenLookup("fever.login", $hash);
|
||||||
} catch (\JKingWeb\Arsse\Db\ExceptionInput $e) {
|
} catch (ExceptionInput $e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// set the user name
|
// set the user name
|
||||||
|
@ -263,17 +253,18 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
case "group":
|
case "group":
|
||||||
if ($id > 0) {
|
if ($id > 0) {
|
||||||
// concrete groups
|
// concrete groups
|
||||||
$c->tag($id);
|
$c->tag($id)->hidden(false);
|
||||||
} elseif ($id < 0) {
|
} elseif ($id < 0) {
|
||||||
// group negative-one is the "Sparks" supergroup i.e. no feeds
|
// group negative-one is the "Sparks" supergroup i.e. no feeds
|
||||||
$c->not->folder(0);
|
$c->not->folder(0);
|
||||||
} else {
|
} else {
|
||||||
// group zero is the "Kindling" supergroup i.e. all feeds
|
// group zero is the "Kindling" supergroup i.e. all feeds
|
||||||
// nothing need to be done for this
|
// only exclude hidden articles
|
||||||
|
$c->hidden(false);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "feed":
|
case "feed":
|
||||||
$c->subscription($id);
|
$c->subscription($id)->hidden(false);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
return $listSaved;
|
return $listSaved;
|
||||||
|
@ -308,7 +299,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function setUnread(): void {
|
protected function setUnread(): void {
|
||||||
$lastUnread = Arsse::$db->articleList(Arsse::$user->id, (new Context)->limit(1), ["marked_date"], ["marked_date desc"])->getValue();
|
$lastUnread = Arsse::$db->articleList(Arsse::$user->id, (new Context)->hidden(false)->limit(1), ["marked_date"], ["marked_date desc"])->getValue();
|
||||||
if (!$lastUnread) {
|
if (!$lastUnread) {
|
||||||
// there are no articles
|
// there are no articles
|
||||||
return;
|
return;
|
||||||
|
@ -316,7 +307,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
// Fever takes the date of the last read article less fifteen seconds as a cut-off.
|
// Fever takes the date of the last read article less fifteen seconds as a cut-off.
|
||||||
// We take the date of last mark (whether it be read, unread, saved, unsaved), which
|
// We take the date of last mark (whether it be read, unread, saved, unsaved), which
|
||||||
// may not actually signify a mark, but we'll otherwise also count back fifteen seconds
|
// may not actually signify a mark, but we'll otherwise also count back fifteen seconds
|
||||||
$c = new Context;
|
$c = (new Context)->hidden(false);
|
||||||
$lastUnread = Date::normalize($lastUnread, "sql");
|
$lastUnread = Date::normalize($lastUnread, "sql");
|
||||||
$since = Date::sub("PT15S", $lastUnread);
|
$since = Date::sub("PT15S", $lastUnread);
|
||||||
$c->unread(false)->markedSince($since);
|
$c->unread(false)->markedSince($since);
|
||||||
|
@ -332,7 +323,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
foreach (arsse::$db->subscriptionList(Arsse::$user->id) as $sub) {
|
foreach (arsse::$db->subscriptionList(Arsse::$user->id) as $sub) {
|
||||||
$out[] = [
|
$out[] = [
|
||||||
'id' => (int) $sub['id'],
|
'id' => (int) $sub['id'],
|
||||||
'favicon_id' => 0, // TODO: implement favicons
|
'favicon_id' => (int) $sub['icon_id'],
|
||||||
'title' => (string) $sub['title'],
|
'title' => (string) $sub['title'],
|
||||||
'url' => $sub['url'],
|
'url' => $sub['url'],
|
||||||
'site_url' => $sub['source'],
|
'site_url' => $sub['source'],
|
||||||
|
@ -343,6 +334,24 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function getIcons(): array {
|
||||||
|
$out = [
|
||||||
|
[
|
||||||
|
'id' => 0,
|
||||||
|
'data' => self::GENERIC_ICON_TYPE.",".self::GENERIC_ICON_DATA,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
foreach (Arsse::$db->iconList(Arsse::$user->id) as $icon) {
|
||||||
|
if ($icon['data']) {
|
||||||
|
$out[] = [
|
||||||
|
'id' => (int) $icon['id'],
|
||||||
|
'data' => ($icon['type'] ?: "application/octet-stream").";base64,".base64_encode($icon['data']),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
protected function getGroups(): array {
|
protected function getGroups(): array {
|
||||||
$out = [];
|
$out = [];
|
||||||
foreach (Arsse::$db->tagList(Arsse::$user->id) as $member) {
|
foreach (Arsse::$db->tagList(Arsse::$user->id) as $member) {
|
||||||
|
@ -373,11 +382,11 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getItems(array $G): array {
|
protected function getItems(array $G): array {
|
||||||
$c = (new Context)->limit(50);
|
$c = (new Context)->hidden(false)->limit(50);
|
||||||
$reverse = false;
|
$reverse = false;
|
||||||
// handle the standard options
|
// handle the standard options
|
||||||
if ($G['with_ids']) {
|
if ($G['with_ids']) {
|
||||||
$c->articles(explode(",", $G['with_ids']));
|
$c->articles(explode(",", $G['with_ids']))->hidden(null);
|
||||||
} elseif ($G['max_id']) {
|
} elseif ($G['max_id']) {
|
||||||
$c->latestArticle($G['max_id'] - 1);
|
$c->latestArticle($G['max_id'] - 1);
|
||||||
$reverse = true;
|
$reverse = true;
|
||||||
|
@ -410,7 +419,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getItemIds(Context $c = null): string {
|
protected function getItemIds(Context $c): string {
|
||||||
$out = [];
|
$out = [];
|
||||||
foreach (Arsse::$db->articleList(Arsse::$user->id, $c) as $r) {
|
foreach (Arsse::$db->articleList(Arsse::$user->id, $c) as $r) {
|
||||||
$out[] = (int) $r['id'];
|
$out[] = (int) $r['id'];
|
||||||
|
|
19
lib/REST/Miniflux/ErrorResponse.php
Normal file
19
lib/REST/Miniflux/ErrorResponse.php
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
/** @license MIT
|
||||||
|
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||||
|
* See LICENSE and AUTHORS files for details */
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
namespace JKingWeb\Arsse\REST\Miniflux;
|
||||||
|
|
||||||
|
use JKingWeb\Arsse\Arsse;
|
||||||
|
|
||||||
|
class ErrorResponse extends \Laminas\Diactoros\Response\JsonResponse {
|
||||||
|
public function __construct($data, int $status = 400, array $headers = [], int $encodingOptions = self::DEFAULT_JSON_FLAGS) {
|
||||||
|
assert(isset(Arsse::$lang) && Arsse::$lang instanceof \JKingWeb\Arsse\Lang, new \Exception("Language database must be initialized before use"));
|
||||||
|
$data = (array) $data;
|
||||||
|
$msg = array_shift($data);
|
||||||
|
$data = ["error_message" => Arsse::$lang->msg("API.Miniflux.Error.".$msg, $data)];
|
||||||
|
parent::__construct($data, $status, $headers, $encodingOptions);
|
||||||
|
}
|
||||||
|
}
|
37
lib/REST/Miniflux/Status.php
Normal file
37
lib/REST/Miniflux/Status.php
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<?php
|
||||||
|
/** @license MIT
|
||||||
|
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||||
|
* See LICENSE and AUTHORS files for details */
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
namespace JKingWeb\Arsse\REST\Miniflux;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Laminas\Diactoros\Response\EmptyResponse;
|
||||||
|
use Laminas\Diactoros\Response\TextResponse;
|
||||||
|
|
||||||
|
class Status extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
|
public function __construct() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dispatch(ServerRequestInterface $req): ResponseInterface {
|
||||||
|
$target = parse_url($req->getRequestTarget())['path'] ?? "";
|
||||||
|
if (!in_array($target, ["/version", "/healthcheck"])) {
|
||||||
|
return new EmptyResponse(404);
|
||||||
|
}
|
||||||
|
$method = $req->getMethod();
|
||||||
|
if ($method === "OPTIONS") {
|
||||||
|
return new EmptyResponse(204, ['Allow' => "HEAD, GET"]);
|
||||||
|
} elseif ($method !== "GET") {
|
||||||
|
return new EmptyResponse(405, ['Allow' => "HEAD, GET"]);
|
||||||
|
}
|
||||||
|
$out = "";
|
||||||
|
if ($target === "/version") {
|
||||||
|
$out = V1::VERSION;
|
||||||
|
} elseif ($target === "/healthcheck") {
|
||||||
|
$out = "OK";
|
||||||
|
}
|
||||||
|
return new TextResponse($out);
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue