Added web information to the sledrun lists.
[philipp/winterrodeln/mediawiki_extensions/wrreport.git] / libravatar.php
1 <?php
2 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
3 /**
4  * PHP support for the Libravatar.org service.
5  *
6  * PHP version 5
7  *
8  * The MIT License
9  *
10  * Copyright (c) 2011 Services_Libravatar committers.
11  *
12  * Permission is hereby granted, free of charge, to any person obtaining a copy
13  * of this software and associated documentation files (the "Software"), to deal
14  * in the Software without restriction, including without limitation the rights
15  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16  * copies of the Software, and to permit persons to whom the Software is
17  * furnished to do so, subject to the following conditions:
18  *
19  * The above copyright notice and this permission notice shall be included in
20  * all copies or substantial portions of the Software.
21  *
22  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
28  * THE SOFTWARE.
29  *
30  * @category  Services
31  * @package   Services_Libravatar
32  * @author    Melissa Draper <melissa@meldraweb.com>
33  * @copyright 2011 Services_Libravatar committers.
34  * @license   http://www.opensource.org/licenses/mit-license.html  MIT License
35  * @link      http://pear.php.net/package/Services_Libravatar
36  * @since     File available since Release 0.1.0
37  */
38
39 /**
40  * PHP support for the Libravatar.org service.
41  *
42  * Using this class is easy. After including or requiring
43  * Services/Libravatar.php simply do:
44  * <code>
45  * $libravatar = new Services_Libravatar();
46  * $url = $libravatar->getUrl('melissa@meldraweb.com');
47  * </code>
48  *
49  * This would populate $url with the string:
50  * <code>
51  * http://cdn.libravatar.org/avatar/4db84629c121f2d443d33bdb9fd149bc
52  * </code>
53  *
54  * A complicated lookup using all the options is:
55  * <code>
56  * $libravatar = new Services_Libravatar();
57  * $libravatar->setSize(40);
58  * $libravatar->setAlgorithm('sha256');
59  * $libravatar->setHttps(true);
60  * $libravatar->setDefault(
61  *     'http://upload.wikimedia.org/wikipedia/commons/a/af/Tux.png'
62  * );
63  * $url = $libravatar->getUrl('melissa@meldraweb.com');
64  * </code>
65  *
66  * @category  Services
67  * @package   Services_Libravatar
68  * @author    Melissa Draper <melissa@meldraweb.com>
69  * @copyright 2011 Services_Libravatar committers.
70  * @license   http://www.opensource.org/licenses/mit-license.html  MIT License
71  * @version   Release: 0.2.3
72  * @link      http://pear.php.net/package/Services_Libravatar
73  * @since     Class available since Release 0.1.0
74  */
75 class Services_Libravatar
76 {
77     /**
78      * Hashing algorithm to use
79      *
80      * @var string
81      * @see processAlgorithm()
82      * @see setAlgorithm()
83      */
84     protected $algorithm = 'md5';
85
86     /**
87      * Default image URL to use
88      *
89      * @var string
90      * @see processDefault()
91      * @see setDefault()
92      */
93     protected $default;
94
95     /**
96      * If HTTPS URLs should be used
97      *
98      * @var boolean
99      * @see detectHttps()
100      * @see setHttps()
101      */
102     protected $https;
103
104     /**
105      * Image size in pixels
106      *
107      * @var integer
108      * @see processSize()
109      * @see setSize()
110      */
111     protected $size;
112
113
114     /**
115      * Composes a URL for the identifier and options passed in
116      *
117      * Compose a full URL as specified by the Libravatar API, based on the
118      * email address or openid URL passed in, and the options specified.
119      *
120      * @param string $identifier a string of either an email address
121      *                           or an openid url
122      * @param array  $options    an array of (bool) https, (string) algorithm
123      *                           (string) size, (string) default.
124      *                           See the set* methods.
125      *
126      * @return string A string of a full URL for an avatar image
127      *
128      * @since Method available since Release 0.2.0
129      * @deprecated Use getUrl() instead
130      */
131     public function url($identifier, $options = array())
132     {
133         return $this->getUrl($identifier, $options);
134     }
135
136     /**
137      * Composes a URL for the identifier and options passed in
138      *
139      * Compose a full URL as specified by the Libravatar API, based on the
140      * email address or openid URL passed in, and the options specified.
141      *
142      * @param string $identifier a string of either an email address
143      *                           or an openid url
144      * @param array  $options    an array of (bool) https, (string) algorithm
145      *                           (string) size, (string) default.
146      *                           See the set* methods.
147      *
148      * @return string A string of a full URL for an avatar image
149      *
150      * @since  Method available since Release 0.2.0
151      * @throws InvalidArgumentException When an invalid option is passed
152      */
153     public function getUrl($identifier, $options = array())
154     {
155         // If no identifier has been passed, set it to a null.
156         // This way, there'll always be something returned.
157         if (!$identifier) {
158             $identifier = null;
159         } else {
160             $identifier = $this->normalizeIdentifier($identifier);
161         }
162
163         // Load all options
164         $options = $this->checkOptionsArray($options);
165         $https = $this->https;
166         if (isset($options['https'])) {
167             $https = (bool)$options['https'];
168         }
169
170         $algorithm = $this->algorithm;
171         if (isset($options['algorithm'])) {
172             $algorithm = $this->processAlgorithm($options['algorithm']);
173         }
174
175         $default = $this->default;
176         if (isset($options['default'])) {
177             $default = $this->processDefault($options['default']);
178         }
179         $size = $this->size;
180         if (isset($options['size'])) {
181             $size = $this->processSize($options['size']);
182         }
183
184
185         $identifierHash = $this->identifierHash($identifier, $algorithm);
186
187         // Get the domain so we can determine the SRV stuff for federation
188         $domain = $this->domainGet($identifier);
189
190         // If https has been specified in $options, make sure we make the
191         // correct SRV lookup
192         $service  = $this->srvGet($domain, $https);
193         $protocol = $https ? 'https' : 'http';
194
195         $params = array();
196         if ($size !== null) {
197             $params['size'] = $size;
198         }
199         if ($default !== null) {
200             $params['default'] = $default;
201         }
202         $paramString = '';
203         if (count($params) > 0) {
204             $paramString = '?' . http_build_query($params);
205         }
206
207         // Compose the URL from the pieces we generated
208         $url = $protocol . '://' . $service . '/avatar/' . $identifierHash
209             . $paramString;
210
211         // Return the URL string
212         return $url;
213     }
214
215     /**
216      * Checks the options array and verify that only allowed options are in it.
217      *
218      * @param array $options Array of options for getUrl()
219      *
220      * @return void
221      * @throws Exception When an invalid option is used
222      */
223     protected function checkOptionsArray($options)
224     {
225         //this short options are deprecated!
226         if (isset($options['s'])) {
227             $options['size'] = $options['s'];
228             unset($options['s']);
229         }
230         if (isset($options['d'])) {
231             $options['default'] = $options['d'];
232             unset($options['d']);
233         }
234
235         $allowedOptions = array(
236             'algorithm' => true,
237             'default'   => true,
238             'https'     => true,
239             'size'      => true,
240         );
241         foreach ($options as $key => $value) {
242             if (!isset($allowedOptions[$key])) {
243                 throw new InvalidArgumentException(
244                     'Invalid option in array: ' . $key
245                 );
246             }
247         }
248
249         return $options;
250     }
251
252     /**
253      * Normalizes the identifier (E-mail address or OpenID)
254      *
255      * @param string $identifier E-Mail address or OpenID
256      *
257      * @return string Normalized identifier
258      */
259     protected function normalizeIdentifier($identifier)
260     {
261         if (filter_var($identifier, FILTER_VALIDATE_EMAIL)) {
262             return strtolower($identifier);
263         } else {
264             return self::normalizeOpenId($identifier);
265         }
266     }
267
268     /**
269      * Create a hash of the identifier.
270      *
271      * Create a hash of the email address or openid passed in. Algorithm
272      * used for email address ONLY can be varied. Either md5 or sha256
273      * are supported by the Libravatar API. Will be ignored for openid.
274      *
275      * @param string $identifier A string of the email address or openid URL
276      * @param string $hash       A string of the hash algorithm type to make
277      *                           Uses the php implementation of hash()
278      *                           MD5 preferred for Gravatar fallback
279      *
280      * @return string A string hash of the identifier.
281      *
282      * @since Method available since Release 0.1.0
283      */
284     protected function identifierHash($identifier, $hash = 'md5')
285     {
286         if (filter_var($identifier, FILTER_VALIDATE_EMAIL) || $identifier === null) {
287             // If email, we can select our algorithm. Default to md5 for
288             // gravatar fallback.
289             return hash($hash, $identifier);
290         }
291
292         //no email, so the identifier has to be an OpenID
293         return hash('sha256', $identifier);
294     }
295
296     /**
297      * Normalizes an identifier (URI or XRI)
298      *
299      * @param mixed $identifier URI or XRI to be normalized
300      *
301      * @return string Normalized Identifier.
302      *                Empty string when the OpenID is invalid.
303      *
304      * @internal Adapted from OpenID::normalizeIdentifier()
305      */
306     public static function normalizeOpenId($identifier)
307     {
308         // XRI
309         if (preg_match('@^xri://@i', $identifier)) {
310             return preg_replace('@^xri://@i', '', $identifier);
311         }
312
313         if (in_array($identifier[0], array('=', '@', '+', '$', '!'))) {
314             return $identifier;
315         }
316
317         // URL
318         if (!preg_match('@^http[s]?://@i', $identifier)) {
319             $identifier = 'http://' . $identifier;
320         }
321         if (strpos($identifier, '/', 8) === false) {
322             $identifier .= '/';
323         }
324         if (!filter_var($identifier, FILTER_VALIDATE_URL)) {
325             return '';
326         }
327
328         $parts = parse_url($identifier);
329         $parts['scheme'] = strtolower($parts['scheme']);
330         $parts['host']   = strtolower($parts['host']);
331
332         //http://openid.net/specs/openid-authentication-2_0.html#normalization
333         return $parts['scheme'] . '://'
334             . (isset($parts['user']) ? $parts['user'] : '')
335             . (isset($parts['pass']) ? ':' . $parts['pass'] : '')
336             . (isset($parts['user']) || isset($parts['pass']) ? '@' : '')
337             . $parts['host']
338             . (
339                 (isset($parts['port'])
340                 && $parts['scheme'] === 'http' && $parts['port'] != 80)
341                 || (isset($parts['port'])
342                 && $parts['scheme'] === 'https' && $parts['port'] != 443)
343                 ? ':' . $parts['port'] : ''
344             )
345             . $parts['path']
346             . (isset($parts['query']) ? '?' . $parts['query'] : '');
347             //leave out fragment as requested by the spec
348     }
349
350     /**
351      * Grab the domain from the identifier.
352      *
353      * Extract the domain from the Email or OpenID.
354      *
355      * @param string $identifier A string of the email address or openid URL
356      *
357      * @return string A string of the domain to use
358      *
359      * @since Method available since Release 0.1.0
360      */
361     protected function domainGet($identifier)
362     {
363         if ($identifier === null) {
364             return null;
365         }
366
367         // What are we, email or openid? Split ourself up and get the
368         // important bit out.
369         if (filter_var($identifier, FILTER_VALIDATE_EMAIL)) {
370             $email = explode('@', $identifier);
371             return $email[1];
372         }
373
374         //OpenID
375         $url = parse_url($identifier);
376         if (!isset($url['host'])) {
377             //invalid URL
378             return null;
379         }
380
381         $domain = $url['host'];
382         if (isset($url['port']) && $url['scheme'] === 'http'
383             && $url['port'] != 80
384             || isset($url['port']) && $url['scheme'] === 'https'
385             && $url['port'] != 443
386         ) {
387             $domain .= ':' . $url['port'];
388         }
389
390         return $domain;
391     }
392
393     /**
394      * Get the target to use.
395      *
396      * Get the SRV record, filtered by priority and weight. If our domain
397      * has no SRV records, fall back to Libravatar.org
398      *
399      * @param string  $domain A string of the domain we extracted from the
400      *                        provided identifier with domainGet()
401      * @param boolean $https  Whether or not to look for https records
402      *
403      * @return string The target URL.
404      *
405      * @since Method available since Release 0.1.0
406      */
407     protected function srvGet($domain, $https = false)
408     {
409         // Are we going secure? Set up a fallback too.
410         if (isset($https) && $https === true) {
411             $subdomain = '_avatars-sec._tcp.';
412             $fallback  = 'seccdn.';
413             $port      = 443;
414         } else {
415             $subdomain = '_avatars._tcp.';
416             $fallback  = 'cdn.';
417             $port      = 80;
418         }
419
420         if ($domain === null) {
421             // No domain means invalid email address/openid
422             return $fallback . 'libravatar.org';
423         }
424
425         // Lets try get us some records based on the choice of subdomain
426         // and the domain we had passed in.
427         $srv = dns_get_record($subdomain . $domain, DNS_SRV);
428
429         // Did we get anything? No?
430         if (count($srv) == 0) {
431             // Then let's try Libravatar.org.
432             return $fallback . 'libravatar.org';
433         }
434
435         // Sort by the priority. We must get the lowest.
436         usort($srv, array($this, 'comparePriority'));
437
438         $top = $srv[0];
439         $sum = 0;
440
441         // Try to adhere to RFC2782's weighting algorithm, page 3
442         // "arrange all SRV RRs (that have not been ordered yet) in any order,
443         // except that all those with weight 0 are placed at the beginning of
444         // the list."
445         shuffle($srv);
446         $srvs = array();
447         foreach ($srv as $s) {
448             if ($s['weight'] == 0) {
449                 array_unshift($srvs, $s);
450             } else {
451                 array_push($srvs, $s);
452             }
453         }
454
455         foreach ($srvs as $s) {
456             if ($s['pri'] == $top['pri']) {
457                 // "Compute the sum of the weights of those RRs"
458                 $sum += (int) $s['weight'];
459                 // "and with each RR associate the running sum in the selected
460                 // order."
461                 $pri[$sum] = $s;
462             }
463         }
464
465         // "Then choose a uniform random number between 0 and the sum computed
466         // (inclusive)"
467         $random = rand(0, $sum);
468
469         // "and select the RR whose running sum value is the first in the selected
470         // order which is greater than or equal to the random number selected"
471         foreach ($pri as $k => $v) {
472             if ($k >= $random) {
473                 $target = $v['target'];
474                 if ($v['port'] !== $port) {
475                     $target.= ':'. $v['port'];
476                 }
477                 return $target;
478             }
479         }
480     }
481
482     /**
483      * Sorting function for record priorities.
484      *
485      * @param mixed $a A mixed value passed by usort()
486      * @param mixed $b A mixed value passed by usort()
487      *
488      * @return mixed The result of the comparison
489      *
490      * @since Method available since Release 0.1.0
491      */
492     protected function comparePriority($a, $b)
493     {
494         return $a['pri'] - $b['pri'];
495     }
496
497     /**
498      * Automatically set the https option depending on the current connection
499      * value.
500      *
501      * If the current connection is HTTPS, the https options is activated.
502      * If it is not HTTPS, the https option is deactivated.
503      *
504      * @return self
505      */
506     public function detectHttps()
507     {
508         $this->setHttps(
509             isset($_SERVER['HTTPS']) && $_SERVER['HTTPS']
510         );
511
512         return $this;
513     }
514
515     /**
516      * Verify and cast the email address hashing algorithm to use.
517      *
518      * @param string $algorithm Algorithm to use, "sha256" or "md5".
519      *
520      * @return string Algorithm
521      *
522      * @throws InvalidArgumentException When an unsupported algorithm is given
523      */
524     protected function processAlgorithm($algorithm)
525     {
526         $algorithm = (string)$algorithm;
527         if ($algorithm !== 'md5' && $algorithm !== 'sha256') {
528             throw new InvalidArgumentException(
529                 'Only md5 and sha256 hashing supported'
530             );
531         }
532
533         return $algorithm;
534     }
535
536     /**
537      * Verify and cast the default URL to use when no avatar image can be found.
538      * If none is set, the libravatar logo is returned.
539      *
540      * @param string $url Full URL to use OR one of the following:
541      *                    - "404" - give a "404 File not found" instead of an image
542      *                    - "mm"
543      *                    - "identicon"
544      *                    - "monsterid"
545      *                    - "wavatar"
546      *                    - "retro"
547      *
548      * @return string Default URL
549      *
550      * @throws InvalidArgumentException When an invalid URL is given
551      */
552     protected function processDefault($url)
553     {
554         if ($url === null) {
555             return $url;
556         }
557
558         $url = (string)$url;
559
560         switch ($url) {
561         case '404':
562         case 'mm':
563         case 'identicon':
564         case 'monsterid':
565         case 'wavatar':
566         case 'retro':
567             break;
568         default:
569             $valid = filter_var($url, FILTER_VALIDATE_URL);
570             if (!$valid) {
571                 throw new InvalidArgumentException('Invalid default avatar URL');
572             }
573             break;
574         }
575
576         return $url;
577     }
578
579     /**
580      * Verify and cast the required size of the images.
581      *
582      * @param integer $size Size (width and height in pixels) of the image.
583      *                      NULL for the default width.
584      *
585      * @return integer Size
586      *
587      * @throws InvalidArgumentException When a size <= 0 is given
588      */
589     protected function processSize($size)
590     {
591         if ($size === null) {
592             return $size;
593         }
594
595         $size = (int)$size;
596         if ($size <= 0) {
597             throw new InvalidArgumentException('Size has to be larger than 0');
598         }
599
600         return (int)$size;
601     }
602
603
604     /**
605      * Set the email address hashing algorithm to use.
606      * To keep gravatar compatibility, use "md5".
607      *
608      * @param string $algorithm Algorithm to use, "sha256" or "md5".
609      *
610      * @return self
611      * @throws InvalidArgumentException When an unsupported algorithm is given
612      */
613     public function setAlgorithm($algorithm)
614     {
615         $this->algorithm = $this->processAlgorithm($algorithm);
616
617         return $this;
618     }
619
620     /**
621      * Set the default URL to use when no avatar image can be found.
622      * If none is set, the gravatar logo is returned.
623      *
624      * @param string $url Full URL to use OR one of the following:
625      *                    - "404" - give a "404 File not found" instead of an image
626      *                    - "mm"
627      *                    - "identicon"
628      *                    - "monsterid"
629      *                    - "wavatar"
630      *                    - "retro"
631      *
632      * @return self
633      * @throws InvalidArgumentException When an invalid URL is given
634      */
635     public function setDefault($url)
636     {
637         $this->default = $this->processDefault($url);
638
639         return $this;
640     }
641
642     /**
643      * Set if HTTPS URLs shall be returned.
644      *
645      * @param boolean $useHttps If HTTPS url shall be returned
646      *
647      * @return self
648      *
649      * @see detectHttps()
650      */
651     public function setHttps($useHttps)
652     {
653         $this->https = (bool)$useHttps;
654
655         return $this;
656     }
657
658     /**
659      * Set the required size of the images.
660      * Every avatar image is square sized, which means you need to set only number.
661      *
662      * @param integer $size Size (width and height) of the image
663      *
664      * @return self
665      * @throws InvalidArgumentException When a size <= 0 is given
666      */
667     public function setSize($size)
668     {
669         $this->size = $this->processSize($size);
670
671         return $this;
672     }
673
674 }
675
676 /*
677  * Local variables:
678  * tab-width: 4
679  * c-basic-offset: 4
680  * c-hanging-comment-ender-p: nil
681  * End:
682  */
683
684 ?>