xref: /dokuwiki/inc/Feed/FeedParserFile.php (revision 9255329a6ed66938c8cd0229ee2fbb1f65786fa2)
1<?php
2
3namespace dokuwiki\Feed;
4
5use dokuwiki\HTTP\DokuHTTPClient;
6use SimplePie\File;
7
8/**
9 * Fetch an URL using our own HTTPClient
10 *
11 * Replaces SimplePie's own File class.
12 */
13class FeedParserFile extends File
14{
15    /** @var DokuHTTPClient */
16    protected $http;
17
18    /** @var string the requested URL */
19    protected $requestUrl;
20    /** @var int the HTTP status code of the response */
21    protected $responseStatus;
22    /** @var array<string, string[]> response headers in SimplePie's representation */
23    protected $responseHeaders = [];
24    /** @var string the response body */
25    protected $responseBody = '';
26
27    /** @noinspection PhpMissingParentConstructorInspection */
28
29    /**
30     * Fetches the given URL through DokuHTTPClient
31     *
32     * SimplePie creates this object through its registry and reads the response via the
33     * get_*() methods below, so the fetch has to happen here in the constructor.
34     *
35     * @inheritdoc
36     */
37    public function __construct($url)
38    {
39        $this->http = $this->initHTTPClient();
40        $this->success = $this->http->sendRequest($url);
41
42        $this->requestUrl = $url;
43        // DokuHTTPClient reports transport failures as negative pseudo statuses (-100 etc.),
44        // but SimplePie only surfaces our error message when the status code is 0
45        $this->responseStatus = max(0, (int)$this->http->status);
46        $this->responseBody = (string)$this->http->resp_body;
47        $this->responseHeaders = $this->normalizeHeaders($this->http->resp_headers);
48        // DokuHTTPClient uses an empty string for "no error", but SimplePie's FileClient
49        // treats any non-null error combined with a zero status code as a failed request
50        $this->error = $this->http->error ?: null;
51    }
52
53    /**
54     * Creates the HTTP client used to fetch the feed
55     *
56     * Separated out so tests can inject a client with a canned response
57     *
58     * @return DokuHTTPClient
59     */
60    protected function initHTTPClient()
61    {
62        return new DokuHTTPClient();
63    }
64
65    /**
66     * Converts DokuHTTPClient's "name => value" headers into SimplePie's
67     * "name => [values]" representation
68     *
69     * A header that occurred more than once is stored by DokuHTTPClient as an array
70     * of values, so each value is normalized on its own and comma-separated lists are
71     * split into their individual parts.
72     *
73     * @param array<string, string|string[]> $headers
74     * @return array<string, string[]>
75     */
76    protected function normalizeHeaders($headers)
77    {
78        $normalized = [];
79        foreach ($headers as $name => $value) {
80            $values = [];
81            foreach ((array)$value as $line) {
82                foreach (explode(',', (string)$line) as $part) {
83                    $values[] = trim($part);
84                }
85            }
86            $normalized[$name] = $values;
87        }
88        return $normalized;
89    }
90
91    // the following methods implement SimplePie's Response interface and have to keep its naming
92    // phpcs:disable PSR1.Methods.CamelCapsMethodName.NotCamelCaps
93
94    /** @inheritdoc */
95    public function get_final_requested_uri(): string
96    {
97        return (string)$this->requestUrl;
98    }
99
100    /** @inheritdoc */
101    public function get_permanent_uri(): string
102    {
103        return (string)$this->requestUrl;
104    }
105
106    /** @inheritdoc */
107    public function get_status_code(): int
108    {
109        return $this->responseStatus;
110    }
111
112    /** @inheritdoc */
113    public function get_headers(): array
114    {
115        return $this->responseHeaders;
116    }
117
118    /** @inheritdoc */
119    public function has_header(string $name): bool
120    {
121        return isset($this->responseHeaders[strtolower($name)]);
122    }
123
124    /** @inheritdoc */
125    public function get_header(string $name): array
126    {
127        return $this->responseHeaders[strtolower($name)] ?? [];
128    }
129
130    /** @inheritdoc */
131    public function get_header_line(string $name): string
132    {
133        return implode(', ', $this->get_header($name));
134    }
135
136    /** @inheritdoc */
137    public function get_body_content(): string
138    {
139        return $this->responseBody;
140    }
141
142    // phpcs:enable PSR1.Methods.CamelCapsMethodName.NotCamelCaps
143}
144