Add unit test preparation
[phinde.git] / src / phinde / HubUrlExtractor.php
1 <?php
2 namespace phinde;
3
4 class HubUrlExtractor
5 {
6     /**
7      * HTTP request object that's used to do the requests
8      *
9      * @var \HTTP_Request2
10      */
11     protected $request;
12
13     /**
14      * Get the hub and self/canonical URL of a given topic URL.
15      * Uses link headers and parses HTML link rels.
16      *
17      * @param string $url Topic URL
18      *
19      * @return array Array of URLs with keys: hub, self
20      */
21     public function getUrls($url)
22     {
23         //at first, try a HEAD request that does not transfer so much data
24         $req = $this->getRequest();
25         $req->setUrl($url);
26         $req->setMethod(\HTTP_Request2::METHOD_HEAD);
27         $res = $req->send();
28
29         if (intval($res->getStatus() / 100) >= 4
30             && $res->getStatus() != 405 //method not supported/allowed
31         ) {
32             return null;
33         }
34
35         $url  = $res->getEffectiveUrl();
36         $base = new \Net_URL2($url);
37
38         $urls = $this->extractHeader($res);
39         if (count($urls) === 2) {
40             return $this->absolutifyUrls($urls, $base);
41         }
42
43         list($type) = explode(';', $res->getHeader('Content-type'));
44         if ($type != 'text/html' && $type != 'text/xml'
45             && $type != 'application/xhtml+xml'
46             //FIXME: atom, rss
47             && $res->getStatus() != 405//HEAD method not allowed
48         ) {
49             //we will not be able to extract links from the content
50             return $urls;
51         }
52
53         //HEAD failed, do a normal GET
54         $req->setMethod(\HTTP_Request2::METHOD_GET);
55         $res = $req->send();
56         if (intval($res->getStatus() / 100) >= 4) {
57             return $urls;
58         }
59
60         //yes, maybe the server does return this header now
61         // e.g. PHP's Phar::webPhar() does not work with HEAD
62         // https://bugs.php.net/bug.php?id=51918
63         $urls = array_merge($this->extractHeader($res), $urls);
64         if (count($urls) === 2) {
65             return $this->absolutifyUrls($urls, $base);
66         }
67
68         //FIXME: atom/rss
69         $body = $res->getBody();
70         $doc = $this->loadHtml($body, $res);
71
72         $xpath = new \DOMXPath($doc);
73         $xpath->registerNamespace('h', 'http://www.w3.org/1999/xhtml');
74
75         $nodeList = $xpath->query(
76             '/*[self::html or self::h:html]'
77             . '/*[self::head or self::h:head]'
78             . '/*[(self::link or self::h:link)'
79             . ' and'
80             . ' ('
81             . '  contains(concat(" ", normalize-space(@rel), " "), " hub ")'
82             . '  or'
83             . '  contains(concat(" ", normalize-space(@rel), " "), " canonical ")'
84             . '  or'
85             . '  contains(concat(" ", normalize-space(@rel), " "), " self ")'
86             . ' )'
87             . ']'
88         );
89
90         if ($nodeList->length == 0) {
91             //topic has no links
92             return $urls;
93         }
94
95         foreach ($nodeList as $link) {
96             $uri  = $link->attributes->getNamedItem('href')->nodeValue;
97             $types = explode(
98                 ' ', $link->attributes->getNamedItem('rel')->nodeValue
99             );
100             foreach ($types as $type) {
101                 if ($type == 'canonical') {
102                     $type = 'self';
103                 }
104                 if ($type == 'hub' || $type == 'self'
105                     && !isset($urls[$type])
106                 ) {
107                     $urls[$type] = $uri;
108                 }
109             }
110         }
111
112         //FIXME: base href
113         return $this->absolutifyUrls($urls, $base);
114     }
115
116     /**
117      * Extract hub url from the HTTP response headers.
118      *
119      * @param object $res HTTP response
120      *
121      * @return array Array with maximal two keys: hub and self
122      */
123     protected function extractHeader(\HTTP_Request2_Response $res)
124     {
125         $http = new \HTTP2();
126
127         $urls = array();
128         $links = $http->parseLinks($res->getHeader('Link'));
129         foreach ($links as $link) {
130             if (isset($link['_uri']) && isset($link['rel'])) {
131                 if (!isset($urls['hub'])
132                     && array_search('hub', $link['rel']) !== false
133                 ) {
134                     $urls['hub'] = $link['_uri'];
135                 }
136                 if (!isset($urls['self'])
137                     && array_search('self', $link['rel']) !== false
138                 ) {
139                     $urls['self'] = $link['_uri'];
140                 }
141             }
142         }
143         return $urls;
144     }
145
146     /**
147      * Load a DOMDocument from the given HTML or XML
148      *
149      * @param string $sourceBody Content of $source URI
150      * @param object $res        HTTP response from fetching $source
151      *
152      * @return \DOMDocument DOM document object with HTML/XML loaded
153      */
154     protected static function loadHtml($sourceBody, \HTTP_Request2_Response $res)
155     {
156         $doc = new \DOMDocument();
157
158         libxml_clear_errors();
159         $old = libxml_use_internal_errors(true);
160
161         $typeParts = explode(';', $res->getHeader('content-type'));
162         $type = $typeParts[0];
163         if ($type == 'application/xhtml+xml'
164             || $type == 'application/xml'
165             || $type == 'text/xml'
166         ) {
167             $doc->loadXML($sourceBody);
168         } else {
169             $doc->loadHTML($sourceBody);
170         }
171
172         libxml_clear_errors();
173         libxml_use_internal_errors($old);
174
175         return $doc;
176     }
177
178     /**
179      * Returns the HTTP request object clone that can be used
180      * for one HTTP request.
181      *
182      * @return HTTP_Request2 Clone of the setRequest() object
183      */
184     public function getRequest()
185     {
186         if ($this->request === null) {
187             $request = new \HTTP_Request2();
188             $request->setConfig('follow_redirects', true);
189             $this->setRequestTemplate($request);
190         }
191
192         //we need to clone because previous requests could have
193         //set internal variables like POST data that we don't want now
194         return clone $this->request;
195     }
196
197     /**
198      * Sets a custom HTTP request object that will be used to do HTTP requests
199      *
200      * @param object $request Request object
201      *
202      * @return self
203      */
204     public function setRequestTemplate(\HTTP_Request2 $request)
205     {
206         $this->request = $request;
207         return $this;
208     }
209
210     /**
211      * Make the list of urls absolute
212      *
213      * @param array  $urls Array of maybe relative URLs
214      * @param object $base Base URL to resolve the relatives against
215      *
216      * @return array List of absolute URLs
217      */
218     protected function absolutifyUrls($urls, \Net_URL2 $base)
219     {
220         foreach ($urls as $key => $url) {
221             $urls[$key] = (string) $base->resolve($url);
222         }
223         return $urls;
224     }
225 }
226 ?>