]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/OStatus/lib/feeddiscovery.php
Merge branch 'master' into testing
[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 /**
52  * Given a web page or feed URL, discover the final location of the feed
53  * and return its current contents.
54  *
55  * @example
56  *   $feed = new FeedDiscovery();
57  *   if ($feed->discoverFromURL($url)) {
58  *     print $feed->uri;
59  *     print $feed->type;
60  *     processFeed($feed->body);
61  *   }
62  */
63 class FeedDiscovery
64 {
65     public $uri;
66     public $type;
67     public $body;
68
69
70     public function feedMunger()
71     {
72         require_once 'XML/Feed/Parser.php';
73         $feed = new XML_Feed_Parser($this->body, false, false, true); // @fixme
74         return new FeedMunger($feed, $this->uri);
75     }
76
77     /**
78      * @param string $url
79      * @param bool $htmlOk pass false here if you don't want to follow web pages.
80      * @return string with validated URL
81      * @throws FeedSubBadURLException
82      * @throws FeedSubBadHtmlException
83      * @throws FeedSubNoFeedException
84      * @throws FeedSubEmptyException
85      * @throws FeedSubUnrecognizedTypeException
86      */
87     function discoverFromURL($url, $htmlOk=true)
88     {
89         try {
90             $client = new HTTPClient();
91             $response = $client->get($url);
92         } catch (HTTP_Request2_Exception $e) {
93             throw new FeedSubBadURLException($e);
94         }
95
96         if ($htmlOk) {
97             $type = $response->getHeader('Content-Type');
98             $isHtml = preg_match('!^(text/html|application/xhtml\+xml)!i', $type);
99             if ($isHtml) {
100                 $target = $this->discoverFromHTML($response->getUrl(), $response->getBody());
101                 if (!$target) {
102                     throw new FeedSubNoFeedException($url);
103                 }
104                 return $this->discoverFromURL($target, false);
105             }
106         }
107         
108         return $this->initFromResponse($response);
109     }
110     
111     function initFromResponse($response)
112     {
113         if (!$response->isOk()) {
114             throw new FeedSubBadResponseException($response->getCode());
115         }
116
117         $sourceurl = $response->getUrl();
118         $body = $response->getBody();
119         if (!$body) {
120             throw new FeedSubEmptyException($sourceurl);
121         }
122
123         $type = $response->getHeader('Content-Type');
124         if (preg_match('!^(text/xml|application/xml|application/(rss|atom)\+xml)!i', $type)) {
125             $this->uri = $sourceurl;
126             $this->type = $type;
127             $this->body = $body;
128             return true;
129         } else {
130             common_log(LOG_WARNING, "Unrecognized feed type $type for $sourceurl");
131             throw new FeedSubUnrecognizedTypeException($type);
132         }
133     }
134
135     /**
136      * @param string $url source URL, used to resolve relative links
137      * @param string $body HTML body text
138      * @return mixed string with URL or false if no target found
139      */
140     function discoverFromHTML($url, $body)
141     {
142         // DOMDocument::loadHTML may throw warnings on unrecognized elements.
143         $old = error_reporting(error_reporting() & ~E_WARNING);
144         $dom = new DOMDocument();
145         $ok = $dom->loadHTML($body);
146         error_reporting($old);
147
148         if (!$ok) {
149             throw new FeedSubBadHtmlException();
150         }
151
152         // Autodiscovery links may be relative to the page's URL or <base href>
153         $base = false;
154         $nodes = $dom->getElementsByTagName('base');
155         for ($i = 0; $i < $nodes->length; $i++) {
156             $node = $nodes->item($i);
157             if ($node->hasAttributes()) {
158                 $href = $node->attributes->getNamedItem('href');
159                 if ($href) {
160                     $base = trim($href->value);
161                 }
162             }
163         }
164         if ($base) {
165             $base = $this->resolveURI($base, $url);
166         } else {
167             $base = $url;
168         }
169
170         // Ok... now on to the links!
171         // Types listed in order of priority -- we'll prefer Atom if available.
172         // @fixme merge with the munger link checks
173         $feeds = array(
174             'application/atom+xml' => false,
175             'application/rss+xml' => false,
176         );
177         
178         $nodes = $dom->getElementsByTagName('link');
179         for ($i = 0; $i < $nodes->length; $i++) {
180             $node = $nodes->item($i);
181             if ($node->hasAttributes()) {
182                 $rel = $node->attributes->getNamedItem('rel');
183                 $type = $node->attributes->getNamedItem('type');
184                 $href = $node->attributes->getNamedItem('href');
185                 if ($rel && $type && $href) {
186                     $rel = trim($rel->value);
187                     $type = trim($type->value);
188                     $href = trim($href->value);
189
190                     if (trim($rel) == 'alternate' && array_key_exists($type, $feeds) && empty($feeds[$type])) {
191                         // Save the first feed found of each type...
192                         $feeds[$type] = $this->resolveURI($href, $base);
193                     }
194                 }
195             }
196         }
197
198         // Return the highest-priority feed found
199         foreach ($feeds as $type => $url) {
200             if ($url) {
201                 return $url;
202             }
203         }
204
205         return false;
206     }
207
208     /**
209      * Resolve a possibly relative URL against some absolute base URL
210      * @param string $rel relative or absolute URL
211      * @param string $base absolute URL
212      * @return string absolute URL, or original URL if could not be resolved.
213      */
214     function resolveURI($rel, $base)
215     {
216         require_once "Net/URL2.php";
217         try {
218             $relUrl = new Net_URL2($rel);
219             if ($relUrl->isAbsolute()) {
220                 return $rel;
221             }
222             $baseUrl = new Net_URL2($base);
223             $absUrl = $baseUrl->resolve($relUrl);
224             return $absUrl->getURL();
225         } catch (Exception $e) {
226             common_log(LOG_WARNING, 'Unable to resolve relative link "' .
227                 $rel . '" against base "' . $base . '": ' . $e->getMessage());
228             return $rel;
229         }
230     }
231 }