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