diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6aeb9de..ca9aecb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,8 +10,15 @@ permissions: jobs: php: - name: PHP + name: PHP (FreshRSS ${{ matrix.freshrss }}) runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + # Both ends of the supported range. Analysing only the development tip is how a call to + # a core method that exists solely on edge -- FreshRSS_http_Util::getCurlResolveInfo(), + # added after 1.29.1 -- reached users as a fatal error on every released version. + freshrss: [edge, '1.29.0'] steps: # This extension cannot be analysed on its own: every class it touches # (Minz_Extension, FreshRSS_Feed, …) and the global helpers (_t, _i, _url) @@ -20,7 +27,7 @@ jobs: uses: actions/checkout@v7 with: repository: FreshRSS/FreshRSS - ref: edge + ref: ${{ matrix.freshrss }} path: FreshRSS - name: Check out this extension inside it diff --git a/README.md b/README.md index 705a4d4..e8dc72a 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,17 @@ push as a feed. This runs alongside core's WebSub support and shares nothing with it. A feed advertising both a hub and a cloud will be subscribed through both. +## Requirements + +**FreshRSS 1.29.0 or newer.** The binding constraint is `Minz_HookType::FeedsListBeforeActualize`, +which core added in that release. + +One behaviour is version-dependent rather than required. When walking a feed's permanent redirects, +each hop is re-checked against the internal-host allowlist through +`FreshRSS_http_Util::getCurlResolveInfo()`, which core added after 1.29.1. Cores without it skip +that check and probe without DNS pinning — the same exposure those versions already carry for every +feed fetch, since their `httpGet()` did not pin DNS either. + ## Discovery | Resource | Advertisement | Read from | diff --git a/RssCloud/Redirects.php b/RssCloud/Redirects.php index 5fc33b6..fec1ca9 100644 --- a/RssCloud/Redirects.php +++ b/RssCloud/Redirects.php @@ -148,11 +148,16 @@ private static function permanentLocation(string $url, array $attributes): strin return false; } - // Re-checked at every hop, so a redirect cannot walk into the private network. - $resolve = FreshRSS_http_Util::getCurlResolveInfo($url); - if (!is_array($resolve)) { - // null: the host's IP is not in the allowlist. false: the host did not resolve. - return false; + // Re-checked at every hop, so a redirect cannot walk into the private network. Cores older than + // FreshRSS 1.29.2 have no such check to offer — `httpGet()` did not pin DNS there either, so the + // probe goes ahead unpinned rather than disabling redirect resolution on every released version. + $resolve = []; + if (method_exists(FreshRSS_http_Util::class, 'getCurlResolveInfo')) { + $resolve = FreshRSS_http_Util::getCurlResolveInfo($url); + if (!is_array($resolve)) { + // null: the host's IP is not in the allowlist. false: the host did not resolve. + return false; + } } $ch = curl_init(); diff --git a/phpstan.neon b/phpstan.neon index eaee51a..3adfdeb 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -23,3 +23,21 @@ parameters: scanDirectories: - ../../app - ../../lib + + # `getCurlResolveInfo()` arrived after 1.29.1, so whether it exists depends on which core is + # checked out alongside -- something PHPStan cannot model from one checkout. Whichever core it + # analyses, it calls the runtime guard redundant, just from opposite directions. Both entries + # are listed so a single config serves either; `reportUnmatchedIgnoredErrors: false` above lets + # the inapplicable one pass unmatched. Drop the guard and this whole block once 1.29.2 is the + # oldest supported release. + ignoreErrors: + - + # Against edge, which has the method. + identifier: function.alreadyNarrowedType + message: '#Call to function method_exists\(\) with .FreshRSS_http_Util. and .getCurlResolveInfo. will always evaluate to true\.#' + path: RssCloud/Redirects.php + - + # Against 1.29.1 and older, which do not. + identifier: function.impossibleType + message: '#Call to function method_exists\(\) with .FreshRSS_http_Util. and .getCurlResolveInfo. will always evaluate to false\.#' + path: RssCloud/Redirects.php