diff --git a/CHANGELOG b/CHANGELOG
index 153dfeb5..3e48ece9 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -6,6 +6,7 @@ Bug fixes:
- Don't fail adding a feed which collides with another via redirection
- Don't fail on very long text-search queries containing question marks
when using PostgreSQL or MySQL
+- Specify HTTP authentication encoding as UTF-8
Changes:
- Include a user manual
diff --git a/lib/Misc/HTTP.php b/lib/Misc/HTTP.php
new file mode 100644
index 00000000..78a817b7
--- /dev/null
+++ b/lib/Misc/HTTP.php
@@ -0,0 +1,22 @@
+getHeaderLine("Content-Type") ?? "";
+ foreach ($type as $t) {
+ $pattern = "/^".preg_quote(trim($t), "/")."($|;|,)/i";
+ if (preg_match($pattern, $header)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/lib/REST/Fever/API.php b/lib/REST/Fever/API.php
index 1401d63a..a83b3e1a 100644
--- a/lib/REST/Fever/API.php
+++ b/lib/REST/Fever/API.php
@@ -11,6 +11,7 @@ use JKingWeb\Arsse\Context\Context;
use JKingWeb\Arsse\Misc\ValueInfo as V;
use JKingWeb\Arsse\Misc\Date;
use JKingWeb\Arsse\Db\ExceptionInput;
+use JKingWeb\Arsse\Misc\HTTP;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
use Zend\Diactoros\Response\JsonResponse;
@@ -21,6 +22,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
const LEVEL = 3;
const GENERIC_ICON_TYPE = "image/png;base64";
const GENERIC_ICON_DATA = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAZdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuMjHxIGmVAAAADUlEQVQYV2NgYGBgAAAABQABijPjAAAAAABJRU5ErkJggg==";
+ const ACCEPTED_TYPE = "application/x-www-form-urlencoded";
// GET parameters for which we only check presence: these will be converted to booleans
const PARAM_BOOL = ["groups", "feeds", "items", "favicons", "links", "unread_item_ids", "saved_item_ids"];
@@ -66,11 +68,11 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
case "OPTIONS":
return new EmptyResponse(204, [
'Allow' => "POST",
- 'Accept' => "application/x-www-form-urlencoded",
+ 'Accept' => self::ACCEPTED_TYPE,
]);
case "POST":
- if (strlen($req->getHeaderLine("Content-Type")) && $req->getHeaderLine("Content-Type") !== "application/x-www-form-urlencoded") {
- return new EmptyResponse(415, ['Accept' => "application/x-www-form-urlencoded"]);
+ if (!HTTP::matchType($req, self::ACCEPTED_TYPE, "")) {
+ return new EmptyResponse(415, ['Accept' => self::ACCEPTED_TYPE]);
}
$out = [
'api_version' => self::LEVEL,
diff --git a/lib/REST/NextCloudNews/V1_2.php b/lib/REST/NextCloudNews/V1_2.php
index f64d9cb2..29652d17 100644
--- a/lib/REST/NextCloudNews/V1_2.php
+++ b/lib/REST/NextCloudNews/V1_2.php
@@ -13,6 +13,7 @@ use JKingWeb\Arsse\Misc\ValueInfo;
use JKingWeb\Arsse\AbstractException;
use JKingWeb\Arsse\Db\ExceptionInput;
use JKingWeb\Arsse\Feed\Exception as FeedException;
+use JKingWeb\Arsse\Misc\HTTP;
use JKingWeb\Arsse\REST\Exception404;
use JKingWeb\Arsse\REST\Exception405;
use Psr\Http\Message\ServerRequestInterface;
@@ -23,6 +24,7 @@ use Zend\Diactoros\Response\EmptyResponse;
class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
const REALM = "NextCloud News API v1-2";
const VERSION = "11.0.5";
+ const ACCEPTED_TYPE = "application/json";
protected $dateFormat = "unix";
@@ -90,15 +92,10 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
}
// normalize the input
$data = (string) $req->getBody();
- $type = "";
- if ($req->hasHeader("Content-Type")) {
- $type = $req->getHeader("Content-Type");
- $type = array_pop($type);
- }
if ($data) {
// if the entity body is not JSON according to content type, return "415 Unsupported Media Type"
- if (!preg_match("<^application/json\b|^$>", $type)) {
- return new EmptyResponse(415, ['Accept' => "application/json"]);
+ if (!HTTP::matchType($req, "", self::ACCEPTED_TYPE)) {
+ return new EmptyResponse(415, ['Accept' => self::ACCEPTED_TYPE]);
}
$data = @json_decode($data, true);
if (json_last_error() !== \JSON_ERROR_NONE) {
@@ -269,7 +266,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
}
return new EmptyResponse(204, [
'Allow' => implode(",", $allowed),
- 'Accept' => "application/json",
+ 'Accept' => self::ACCEPTED_TYPE,
]);
} else {
// if the path is not supported, return 404
diff --git a/lib/REST/TinyTinyRSS/API.php b/lib/REST/TinyTinyRSS/API.php
index 045710cf..56a24374 100644
--- a/lib/REST/TinyTinyRSS/API.php
+++ b/lib/REST/TinyTinyRSS/API.php
@@ -43,6 +43,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
const CAT_NOT_SPECIAL = -3;
const CAT_ALL = -4;
// valid input
+ const ACCEPTED_TYPES = ["application/json", "text/json"];
const VALID_INPUT = [
'op' => ValueInfo::T_STRING, // the function ("operation") to perform
'sid' => ValueInfo::T_STRING, // session ID
@@ -99,7 +100,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
// respond to OPTIONS rquests; the response is a fib, as we technically accept any type or method
return new EmptyResponse(204, [
'Allow' => "POST",
- 'Accept' => "application/json, text/json",
+ 'Accept' => implode(", ", self::ACCEPTED_TYPES),
]);
}
$data = (string) $req->getBody();
diff --git a/tests/cases/Misc/TestHTTP.php b/tests/cases/Misc/TestHTTP.php
new file mode 100644
index 00000000..5d6f4656
--- /dev/null
+++ b/tests/cases/Misc/TestHTTP.php
@@ -0,0 +1,32 @@
+withHeader("Content-Type", $header);
+ $this->assertSame($exp, HTTP::matchType($msg, ...$types));
+ $msg = (new \Zend\Diactoros\Response)->withHeader("Content-Type", $header);
+ $this->assertSame($exp, HTTP::matchType($msg, ...$types));
+ }
+
+ public function provideMediaTypes() {
+ return [
+ ["application/json", ["application/json"], true],
+ ["APPLICATION/JSON", ["application/json"], true],
+ ["text/JSON", ["application/json", "text/json"], true],
+ ["text/json; charset=utf-8", ["application/json", "text/json"], true],
+ ["", ["application/json"], false],
+ ["", ["application/json", ""], true],
+ ];
+ }
+}
diff --git a/tests/phpunit.dist.xml b/tests/phpunit.dist.xml
index aeb1dbf3..62e3238e 100644
--- a/tests/phpunit.dist.xml
+++ b/tests/phpunit.dist.xml
@@ -47,6 +47,7 @@
cases/Misc/TestDate.php
cases/Misc/TestContext.php
cases/Misc/TestURL.php
+ cases/Misc/TestHTTP.php
cases/User/TestInternal.php