]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/OStatus/lib/discoveryhints.php
Work around weird bug with HTML normalization via PHP DOM module; if source had xmlns...
[quix0rs-gnu-social.git] / plugins / OStatus / lib / discoveryhints.php
1 <?php
2 /*
3  * StatusNet - the distributed open-source microblogging tool
4  * Copyright (C) 2010, StatusNet, Inc.
5  *
6  * Some utilities for generating hint data
7  *
8  * This program is free software: you can redistribute it and/or modify
9  * it under the terms of the GNU Affero General Public License as published by
10  * the Free Software Foundation, either version 3 of the License, or
11  * (at your option) any later version.
12  *
13  * This program is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  * GNU Affero General Public License for more details.
17  *
18  * You should have received a copy of the GNU Affero General Public License
19  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
20  */
21
22 class DiscoveryHints {
23
24     static function fromXRD($xrd)
25     {
26         $hints = array();
27
28         foreach ($xrd->links as $link) {
29             switch ($link['rel']) {
30             case Discovery::PROFILEPAGE:
31                 $hints['profileurl'] = $link['href'];
32                 break;
33             case Salmon::NS_REPLIES:
34                 $hints['salmon'] = $link['href'];
35                 break;
36             case Discovery::UPDATESFROM:
37                 $hints['feedurl'] = $link['href'];
38                 break;
39             case Discovery::HCARD:
40                 $hints['hcardurl'] = $link['href'];
41                 break;
42             default:
43                 break;
44             }
45         }
46
47         return $hints;
48     }
49
50     static function fromHcardUrl($url)
51     {
52         $client = new HTTPClient();
53         $client->setHeader('Accept', 'text/html,application/xhtml+xml');
54         $response = $client->get($url);
55
56         if (!$response->isOk()) {
57             return null;
58         }
59
60         return self::hcardHints($response->getBody(),
61                                 $response->getUrl());
62     }
63
64     static function hcardHints($body, $url)
65     {
66         common_debug("starting tidy");
67
68         $body = self::_tidy($body, $url);
69
70         common_debug("done with tidy");
71
72         set_include_path(get_include_path() . PATH_SEPARATOR . INSTALLDIR . '/plugins/OStatus/extlib/hkit/');
73         require_once('hkit.class.php');
74
75         // hKit code is not clean for notices and warnings
76         $old = error_reporting();
77         error_reporting($old & ~E_NOTICE & ~E_WARNING);
78
79         $h      = new hKit;
80         $hcards = $h->getByString('hcard', $body);
81
82         error_reporting($old);
83
84         if (empty($hcards)) {
85             return array();
86         }
87
88         if (count($hcards) == 1) {
89             $hcard = $hcards[0];
90         } else {
91             foreach ($hcards as $try) {
92                 if (array_key_exists('url', $try)) {
93                     if (is_string($try['url']) && $try['url'] == $url) {
94                         $hcard = $try;
95                         break;
96                     } else if (is_array($try['url'])) {
97                         foreach ($try['url'] as $tryurl) {
98                             if ($tryurl == $url) {
99                                 $hcard = $try;
100                                 break 2;
101                             }
102                         }
103                     }
104                 }
105             }
106             // last chance; grab the first one
107             if (empty($hcard)) {
108                 $hcard = $hcards[0];
109             }
110         }
111
112         $hints = array();
113
114         if (array_key_exists('nickname', $hcard)) {
115             $hints['nickname'] = $hcard['nickname'];
116         }
117
118         if (array_key_exists('fn', $hcard)) {
119             $hints['fullname'] = $hcard['fn'];
120         } else if (array_key_exists('n', $hcard)) {
121             $hints['fullname'] = implode(' ', $hcard['n']);
122         }
123
124         if (array_key_exists('photo', $hcard)) {
125             $hints['avatar'] = $hcard['photo'];
126         }
127
128         if (array_key_exists('note', $hcard)) {
129             $hints['bio'] = $hcard['note'];
130         }
131
132         if (array_key_exists('adr', $hcard)) {
133             if (is_string($hcard['adr'])) {
134                 $hints['location'] = $hcard['adr'];
135             } else if (is_array($hcard['adr'])) {
136                 $hints['location'] = implode(' ', $hcard['adr']);
137             }
138         }
139
140         if (array_key_exists('url', $hcard)) {
141             if (is_string($hcard['url'])) {
142                 $hints['homepage'] = $hcard['url'];
143             } else if (is_array($hcard['url'])) {
144                 // HACK get the last one; that's how our hcards look
145                 $hints['homepage'] = $hcard['url'][count($hcard['url'])-1];
146             }
147         }
148
149         return $hints;
150     }
151
152     /**
153      * hKit needs well-formed XML for its parsing.
154      * We'll take the HTML body here and normalize it to XML.
155      *
156      * @param string $body HTML document source, possibly not-well-formed
157      * @param string $url source URL
158      * @return string well-formed XML document source
159      * @throws Exception if HTML parsing failed.
160      */
161     private static function _tidy($body, $url)
162     {
163         if (empty($body)) {
164             throw new Exception("Empty HTML could not be parsed.");
165         }
166         $dom = new DOMDocument();
167
168         // Some HTML errors will trigger warnings, but still work.
169         $old = error_reporting();
170         error_reporting($old & ~E_WARNING);
171         
172         $ok = $dom->loadHTML($body);
173
174         error_reporting($old);
175         
176         if ($ok) {
177             // If the original had xmlns or xml:lang attributes on the
178             // <html>, we seen to end up with duplicates, which causes
179             // parse errors. Remove em!
180             //
181             // For some reason we have to iterate and remove them twice,
182             // *plus* they don't show up on hasAttribute() or removeAttribute().
183             // This might be some weird bug in PHP or libxml2, uncertain if
184             // it affects other folks consistently.
185             $root = $dom->documentElement;
186             foreach ($root->attributes as $i => $x) {
187                 if ($i == 'xmlns' || $i == 'xml:lang') {
188                     $root->removeAttributeNode($x);
189                 }
190             }
191             foreach ($root->attributes as $i => $x) {
192                 if ($i == 'xmlns' || $i == 'xml:lang') {
193                     $root->removeAttributeNode($x);
194                 }
195             }
196
197             // hKit doesn't give us a chance to pass the source URL for
198             // resolving relative links, such as the avatar photo on a
199             // Google profile. We'll slip it into a <base> tag if there's
200             // not already one present.
201             $bases = $dom->getElementsByTagName('base');
202             if ($bases && $bases->length >= 1) {
203                 $base = $bases->item(0);
204                 if ($base->hasAttribute('href')) {
205                     $base->setAttribute('href', $url);
206                 }
207             } else {
208                 $base = $dom->createElement('base');
209                 $base->setAttribute('href', $url);
210                 $heads = $dom->getElementsByTagName('head');
211                 if ($heads || $heads->length) {
212                     $head = $heads->item(0);
213                 } else {
214                     $head = $dom->createElement('head');
215                     if ($root->firstChild) {
216                         $root->insertBefore($head, $root->firstChild);
217                     } else {
218                         $root->appendChild($head);
219                     }
220                 }
221                 $head->appendChild($base);
222             }
223             return $dom->saveXML();
224         } else {
225             throw new Exception("Invalid HTML could not be parsed.");
226         }
227     }
228 }