CI / test (pull_request) Successful in 40s
FreshRSS 1.30.0 added unregister_unsafe_protocols() to lib/lib_rss.php, which unregisters every PHP stream wrapper except file:// and php:// at boot as an SSRF mitigation. That removed the https:// wrapper, so the stream-context-based fetch in apiGet() stopped working entirely — every API call failed with "Unable to find the wrapper https". Because the call site used @file_get_contents(), the failure was silent: apiGet() returned null, loadThread() fell through to its null fallback, and entries were saved with their unenriched RSS content. The visible symptom was Bluesky posts rendering with no thread and no embeds, with nothing in the logs to explain it. Switch apiGet() to cURL, which does not go through stream wrappers (and is how FreshRSS fetches feeds itself, which is why feed retrieval kept working throughout). Connect timeout added and redirect-following explicitly disabled — a public XRPC endpoint has no reason to redirect, and following redirects would reopen an SSRF vector. Also stop swallowing failures: transport errors, non-2xx responses and malformed JSON are now reported via Minz_Log::warning() so this class of breakage is visible next time. Tests: add testApiGetWorksWithoutHttpsStreamWrapper, which unregisters the https wrapper before calling the API and so fails against the old implementation, plus testApiGetLogsWarningOnFailure. Add a Minz_Log stub and curl to the CI extension list. Co-Authored-By: Claude Opus 5 <[email protected]>
103 lines
2.4 KiB
PHP
103 lines
2.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* Minimal stubs for FreshRSS classes so the extension can be loaded and
|
|
* tested outside of a full FreshRSS installation.
|
|
*/
|
|
|
|
abstract class Minz_Extension {
|
|
private array $userConfig = [];
|
|
|
|
public function init(): void {}
|
|
|
|
public function handleConfigureAction(): void {}
|
|
|
|
protected function registerHook(string $name, callable $callback): void {}
|
|
|
|
public function setUserConfigurationValue(string $key, mixed $value): void {
|
|
$this->userConfig[$key] = $value;
|
|
}
|
|
|
|
public function getUserConfigurationValue(string $key): mixed {
|
|
return $this->userConfig[$key] ?? null;
|
|
}
|
|
}
|
|
|
|
class FreshRSS_Entry {
|
|
private string $link = '';
|
|
private string $content = '';
|
|
private int $date = 0;
|
|
|
|
public function link(): string { return $this->link; }
|
|
|
|
/** @param string $link */
|
|
public function _link(string $link): void { $this->link = $link; }
|
|
|
|
/** @param string $content */
|
|
public function _content(string $content): void { $this->content = $content; }
|
|
|
|
public function content(): string { return $this->content; }
|
|
|
|
public function date(bool $asTimestamp = false): mixed {
|
|
return $asTimestamp ? $this->date : date('r', $this->date);
|
|
}
|
|
|
|
public function _date(int $timestamp): void { $this->date = $timestamp; }
|
|
}
|
|
|
|
class FreshRSS_EntryDao {
|
|
public function updateEntry(FreshRSS_Entry $entry): void {}
|
|
}
|
|
|
|
class FreshRSS_Factory {
|
|
public static function createEntryDao(): FreshRSS_EntryDao {
|
|
return new FreshRSS_EntryDao();
|
|
}
|
|
}
|
|
|
|
class Minz_Request {
|
|
private static bool $isPost = false;
|
|
private static array $params = [];
|
|
|
|
public static function isPost(): bool { return self::$isPost; }
|
|
|
|
public static function paramString(string $key): string {
|
|
return self::$params[$key] ?? '';
|
|
}
|
|
|
|
/** Test helper: configure the fake request state. */
|
|
public static function simulatePost(array $params): void {
|
|
self::$isPost = true;
|
|
self::$params = $params;
|
|
}
|
|
|
|
public static function reset(): void {
|
|
self::$isPost = false;
|
|
self::$params = [];
|
|
}
|
|
}
|
|
|
|
class Minz_Log {
|
|
/** @var list<string> */
|
|
private static array $messages = [];
|
|
|
|
public static function warning(string $message): void {
|
|
self::$messages[] = $message;
|
|
}
|
|
|
|
/**
|
|
* Test helper: all warnings recorded since the last reset().
|
|
*
|
|
* @return list<string>
|
|
*/
|
|
public static function messages(): array {
|
|
return self::$messages;
|
|
}
|
|
|
|
public static function reset(): void {
|
|
self::$messages = [];
|
|
}
|
|
}
|