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