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 $this->responseStatus = (int)$this->http->status; 44 $this->responseBody = (string)$this->http->resp_body; 45 $this->responseHeaders = $this->normalizeHeaders($this->http->resp_headers); 46 // DokuHTTPClient uses an empty string for "no error", but SimplePie's FileClient 47 // treats any non-null error combined with a zero status code as a failed request 48 $this->error = $this->http->error ?: null; 49 } 50 51 /** 52 * Creates the HTTP client used to fetch the feed 53 * 54 * Separated out so tests can inject a client with a canned response 55 * 56 * @return DokuHTTPClient 57 */ 58 protected function initHTTPClient() 59 { 60 return new DokuHTTPClient(); 61 } 62 63 /** 64 * Converts DokuHTTPClient's "name => value" headers into SimplePie's 65 * "name => [values]" representation 66 * 67 * A header that occurred more than once is stored by DokuHTTPClient as an array 68 * of values, so each value is normalized on its own and comma-separated lists are 69 * split into their individual parts. 70 * 71 * @param array<string, string|string[]> $headers 72 * @return array<string, string[]> 73 */ 74 protected function normalizeHeaders($headers) 75 { 76 $normalized = []; 77 foreach ($headers as $name => $value) { 78 $values = []; 79 foreach ((array)$value as $line) { 80 foreach (explode(',', (string)$line) as $part) { 81 $values[] = trim($part); 82 } 83 } 84 $normalized[$name] = $values; 85 } 86 return $normalized; 87 } 88 89 // the following methods implement SimplePie's Response interface and have to keep its naming 90 // phpcs:disable PSR1.Methods.CamelCapsMethodName.NotCamelCaps 91 92 /** @inheritdoc */ 93 public function get_final_requested_uri(): string 94 { 95 return (string)$this->requestUrl; 96 } 97 98 /** @inheritdoc */ 99 public function get_permanent_uri(): string 100 { 101 return (string)$this->requestUrl; 102 } 103 104 /** @inheritdoc */ 105 public function get_status_code(): int 106 { 107 return $this->responseStatus; 108 } 109 110 /** @inheritdoc */ 111 public function get_headers(): array 112 { 113 return $this->responseHeaders; 114 } 115 116 /** @inheritdoc */ 117 public function has_header(string $name): bool 118 { 119 return isset($this->responseHeaders[strtolower($name)]); 120 } 121 122 /** @inheritdoc */ 123 public function get_header(string $name): array 124 { 125 return $this->responseHeaders[strtolower($name)] ?? []; 126 } 127 128 /** @inheritdoc */ 129 public function get_header_line(string $name): string 130 { 131 return implode(', ', $this->get_header($name)); 132 } 133 134 /** @inheritdoc */ 135 public function get_body_content(): string 136 { 137 return $this->responseBody; 138 } 139 140 // phpcs:enable PSR1.Methods.CamelCapsMethodName.NotCamelCaps 141} 142