]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/OStatus/lib/feeddiscovery.php
Suppress whinging during HTML parsing in profile page discovery for things that turn...
[quix0rs-gnu-social.git] / plugins / OStatus / lib / feeddiscovery.php
1 <?php
2 /*
3  * StatusNet - the distributed open-source microblogging tool
4  * Copyright (C) 2009, StatusNet, Inc.
5  *
6  * This program is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU Affero General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU Affero General Public License for more details.
15  *
16  * You should have received a copy of the GNU Affero General Public License
17  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 /**
21  * @package FeedSubPlugin
22  * @maintainer Brion Vibber <brion@status.net>
23  */
24
25 if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
26
27 class FeedSubBadURLException extends FeedSubException
28 {
29 }
30
31 class FeedSubBadResponseException extends FeedSubException
32 {
33 }
34
35 class FeedSubEmptyException extends FeedSubException
36 {
37 }
38
39 class FeedSubBadHTMLException extends FeedSubException
40 {
41 }
42
43 class FeedSubUnrecognizedTypeException extends FeedSubException
44 {
45 }
46
47 class FeedSubNoFeedException extends FeedSubException
48 {
49 }
50
51 class FeedSubBadXmlException extends FeedSubException
52 {
53 }
54
55 class FeedSubNoHubException extends FeedSubException
56 {
57 }
58
59 /**
60  * Given a web page or feed URL, discover the final location of the feed
61  * and return its current contents.
62  *
63  * @example
64  *   $feed = new FeedDiscovery();
65  *   if ($feed->discoverFromURL($url)) {
66  *     print $feed->uri;
67  *     print $feed->type;
68  *     processFeed($feed->feed); // DOMDocument
69  *   }
70  */
71 class FeedDiscovery
72 {
73     public $uri;
74     public $type;
75     public $feed;
76     public $root;
77
78     /** Post-initialize query helper... */
79     public function getLink($rel, $type=null)
80     {
81         // @fixme check for non-Atom links in RSS2 feeds as well
82         return self::getAtomLink($rel, $type);
83     }
84
85     public function getAtomLink($rel, $type=null)
86     {
87         return ActivityUtils::getLink($this->root, $rel, $type);
88     }
89
90     /**
91      * Get the referenced PuSH hub link from an Atom feed.
92      *
93      * @return mixed string or false
94      */
95     public function getHubLink()
96     {
97         return $this->getAtomLink('hub');
98     }
99
100     /**
101      * @param string $url
102      * @param bool $htmlOk pass false here if you don't want to follow web pages.
103      * @return string with validated URL
104      * @throws FeedSubBadURLException
105      * @throws FeedSubBadHtmlException
106      * @throws FeedSubNoFeedException
107      * @throws FeedSubEmptyException
108      * @throws FeedSubUnrecognizedTypeException
109      */
110     function discoverFromURL($url, $htmlOk=true)
111     {
112         try {
113             $client = new HTTPClient();
114             $response = $client->get($url);
115         } catch (HTTP_Request2_Exception $e) {
116             common_log(LOG_ERR, __METHOD__ . " Failure for $url - " . $e->getMessage());
117             throw new FeedSubBadURLException($e->getMessage());
118         }
119
120         if ($htmlOk) {
121             $type = $response->getHeader('Content-Type');
122             $isHtml = preg_match('!^(text/html|application/xhtml\+xml)!i', $type);
123             if ($isHtml) {
124                 $target = $this->discoverFromHTML($response->getUrl(), $response->getBody());
125                 if (!$target) {
126                     throw new FeedSubNoFeedException($url);
127                 }
128                 return $this->discoverFromURL($target, false);
129             }
130         }
131
132         return $this->initFromResponse($response);
133     }
134
135     function discoverFromFeedURL($url)
136     {
137         return $this->discoverFromURL($url, false);
138     }
139
140     function initFromResponse($response)
141     {
142         if (!$response->isOk()) {
143             throw new FeedSubBadResponseException($response->getStatus());
144         }
145
146         $sourceurl = $response->getUrl();
147         $body = $response->getBody();
148         if (!$body) {
149             throw new FeedSubEmptyException($sourceurl);
150         }
151
152         $type = $response->getHeader('Content-Type');
153         if (preg_match('!^(text/xml|application/xml|application/(rss|atom)\+xml)!i', $type)) {
154             return $this->init($sourceurl, $type, $body);
155         } else {
156             common_log(LOG_WARNING, "Unrecognized feed type $type for $sourceurl");
157             throw new FeedSubUnrecognizedTypeException($type);
158         }
159     }
160
161     function init($sourceurl, $type, $body)
162     {
163         $feed = new DOMDocument();
164         if ($feed->loadXML($body)) {
165             $this->uri = $sourceurl;
166             $this->type = $type;
167             $this->feed = $feed;
168
169             $el = $this->feed->documentElement;
170
171             // Looking for the "root" element: RSS channel or Atom feed
172
173             if ($el->tagName == 'rss') {
174                 $channels = $el->getElementsByTagName('channel');
175                 if ($channels->length > 0) {
176                     $this->root = $channels->item(0);
177                 } else {
178                     throw new FeedSubBadXmlException($sourceurl);
179                 }
180             } else if ($el->tagName == 'feed') {
181                 $this->root = $el;
182             } else {
183                 throw new FeedSubBadXmlException($sourceurl);
184             }
185
186             return $this->uri;
187         } else {
188             throw new FeedSubBadXmlException($sourceurl);
189         }
190     }
191
192     /**
193      * @param string $url source URL, used to resolve relative links
194      * @param string $body HTML body text
195      * @return mixed string with URL or false if no target found
196      */
197     function discoverFromHTML($url, $body)
198     {
199         // DOMDocument::loadHTML may throw warnings on unrecognized elements,
200         // and notices on unrecognized namespaces.
201         $old = error_reporting(error_reporting() & ~(E_WARNING | E_NOTICE));
202         $dom = new DOMDocument();
203         $ok = $dom->loadHTML($body);
204         error_reporting($old);
205
206         if (!$ok) {
207             throw new FeedSubBadHtmlException();
208         }
209
210         // Autodiscovery links may be relative to the page's URL or <base href>
211         $base = false;
212         $nodes = $dom->getElementsByTagName('base');
213         for ($i = 0; $i < $nodes->length; $i++) {
214             $node = $nodes->item($i);
215             if ($node->hasAttributes()) {
216                 $href = $node->attributes->getNamedItem('href');
217                 if ($href) {
218                     $base = trim($href->value);
219                 }
220             }
221         }
222         if ($base) {
223             $base = $this->resolveURI($base, $url);
224         } else {
225             $base = $url;
226         }
227
228         // Ok... now on to the links!
229         // Types listed in order of priority -- we'll prefer Atom if available.
230         // @fixme merge with the munger link checks
231         $feeds = array(
232             'application/atom+xml' => false,
233             'application/rss+xml' => false,
234         );
235
236         $nodes = $dom->getElementsByTagName('link');
237         for ($i = 0; $i < $nodes->length; $i++) {
238             $node = $nodes->item($i);
239             if ($node->hasAttributes()) {
240                 $rel = $node->attributes->getNamedItem('rel');
241                 $type = $node->attributes->getNamedItem('type');
242                 $href = $node->attributes->getNamedItem('href');
243                 if ($rel && $type && $href) {
244                     $rel = array_filter(explode(" ", $rel->value));
245                     $type = trim($type->value);
246                     $href = trim($href->value);
247
248                     if (in_array('alternate', $rel) && array_key_exists($type, $feeds) && empty($feeds[$type])) {
249                         // Save the first feed found of each type...
250                         $feeds[$type] = $this->resolveURI($href, $base);
251                     }
252                 }
253             }
254         }
255
256         // Return the highest-priority feed found
257         foreach ($feeds as $type => $url) {
258             if ($url) {
259                 return $url;
260             }
261         }
262
263         return false;
264     }
265
266     /**
267      * Resolve a possibly relative URL against some absolute base URL
268      * @param string $rel relative or absolute URL
269      * @param string $base absolute URL
270      * @return string absolute URL, or original URL if could not be resolved.
271      */
272     function resolveURI($rel, $base)
273     {
274         require_once "Net/URL2.php";
275         try {
276             $relUrl = new Net_URL2($rel);
277             if ($relUrl->isAbsolute()) {
278                 return $rel;
279             }
280             $baseUrl = new Net_URL2($base);
281             $absUrl = $baseUrl->resolve($relUrl);
282             return $absUrl->getURL();
283         } catch (Exception $e) {
284             common_log(LOG_WARNING, 'Unable to resolve relative link "' .
285                 $rel . '" against base "' . $base . '": ' . $e->getMessage());
286             return $rel;
287         }
288     }
289 }