test https detection
[services_libravatar:cweiske-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;
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         }
158
159         $https = $this->https;
160         if (isset($options['https'])) {
161             $https = (bool)$options['https'];
162         }
163
164         $algorithm = $this->algorithm;
165         if (isset($options['algorithm'])) {
166             $algorithm = $this->processAlgorithm($options['algorithm']);
167         }
168         $identiferHash = $this->identiferHash($identifier, $https, $algorithm);
169
170         // Get the domain so we can determine the SRV stuff for federation
171         $domain = $this->domainGet($identifier, $https);
172
173         // If https has been specified in $options, make sure we make the
174         // correct SRV lookup
175         $service  = $this->srvGet($domain, $https);
176         $protocol = $https ? 'https' : 'http';
177
178         // Additional URL options
179         $params = array();
180         if ($size !== null) {
181             $params['size'] = $size;
182         }
183         if ($default !== null) {
184             $params['default'] = $default;
185         }
186         if (count($params) > 0) {
187             $paramString = '?' . http_build_query($params);
188         }
189
190         // Compose the URL from the pieces we generated
191         $url = $protocol . '://' . $service . '/avatar/' . $identiferHash
192             . $paramString;
193
194         // Return the URL string
195         return $url;
196     }
197
198     /**
199      * Create a hash of the identifier.
200      *
201      * Create a hash of the email address or openid passed in. Algorithm
202      * used for email address ONLY can be varied. Either md5 or sha256
203      * are supported by the Libravatar API. Will be ignored for openid.
204      *
205      * @param string  $identifier A string of the email address or openid URL
206      * @param boolean $https      If this is https, true.
207      * @param string  $hash       A string of the hash algorithm type to make
208      *                            Uses the php implementation of hash()
209      *                            MD5 preferred for Gravatar fallback
210      *
211      * @return string A string hash of the identifier.
212      *
213      * @since Method available since Release 0.1.0
214      */
215     protected function identiferHash($identifier, $https = false, $hash = 'md5')
216     {
217
218         if (filter_var($identifier, FILTER_VALIDATE_EMAIL)) {
219             // If email, we can select our algorithm. Default to md5 for
220             // gravatar fallback.
221             return hash($hash, $identifier);
222         } else {
223
224             // The protocol is important. If we're lacking it this will not be
225             // filtered. Add it per our preference in the options.
226             if (stripos($identifier, 'http') !== 0) {
227                 if ($https === true) {
228                     $protocol = 'https://';
229                 } else {
230                     $protocol = 'http://';
231                 }
232                 $identifier = $protocol . $identifier;
233             }
234
235             // Is this an email address or an OpenID account
236             $filter = filter_var(
237                 $identifier,
238                 FILTER_VALIDATE_URL,
239                 FILTER_FLAG_PATH_REQUIRED
240             );
241
242             if ($filter) {
243                 // If this is an OpenID, split the string and make sure the
244                 // formatting is correct. See the Libravatar API for more info.
245                 // http://wiki.libravatar.org/api/
246                 $url     = parse_url($identifier);
247                 $hashurl = strtolower($url['scheme']) . '://' .
248                            strtolower($url['host']);
249                 if (isset($url['port']) && $url['scheme'] === 'http'
250                     && $url['port'] != 80
251                     || isset($url['port']) && $url['scheme'] === 'https'
252                     && $url['port'] != 443
253                 ) {
254                     $hashurl .= ':' . $url['port'];
255                 }
256                 $hashurl .= $url['path'];
257                 return hash('sha256', $hashurl);
258             }
259         }
260     }
261
262     /**
263      * Grab the domain from the identifier.
264      *
265      * Extract the domain from the Email or OpenID.
266      *
267      * @param string  $identifier A string of the email address or openid URL
268      * @param boolean $https      If this is https, true.
269      *
270      * @return string A string of the domain to use
271      *
272      * @since Method available since Release 0.1.0
273      */
274     protected function domainGet($identifier, $https = false)
275     {
276
277         // What are we, email or openid? Split ourself up and get the
278         // important bit out.
279         if (filter_var($identifier, FILTER_VALIDATE_EMAIL)) {
280             $email = explode('@', $identifier);
281             return $email[1];
282         } else {
283
284             // The protocol is important. If we're lacking it this will not be
285             // filtered. Add it per our preference in the options.
286             if ( ! strpos($identifier, 'http')) {
287                 if ($https === true) {
288                     $protocol = 'https://';
289                 } else {
290                     $protocol = 'http://';
291                 }
292                 $identifier = $protocol . $identifier;
293             }
294
295             $filter = filter_var(
296                 $identifier,
297                 FILTER_VALIDATE_URL,
298                 FILTER_FLAG_PATH_REQUIRED
299             );
300
301             if ($filter) {
302                 $url    = parse_url($identifier);
303                 $domain = $url['host'];
304                 if (isset($url['port']) && $url['scheme'] === 'http'
305                     && $url['port'] != 80
306                     || isset($url['port']) && $url['scheme'] === 'https'
307                     && $url['port'] != 443
308                 ) {
309                     $domain .= ':' . $url['port'];
310                 }
311
312                 return $domain;
313             }
314         }
315     }
316
317     /**
318      * Get the target to use.
319      *
320      * Get the SRV record, filtered by priority and weight. If our domain
321      * has no SRV records, fall back to Libravatar.org
322      *
323      * @param string  $domain A string of the domain we extracted from the
324      *                        provided identifer with domainGet()
325      * @param boolean $https  Whether or not to look for https records
326      *
327      * @return string The target URL.
328      *
329      * @since Method available since Release 0.1.0
330      */
331     protected function srvGet($domain, $https = false)
332     {
333
334         // Are we going secure? Set up a fallback too.
335         if (isset($https) && $https === true) {
336             $subdomain = '_avatars-sec._tcp.';
337             $fallback  = 'seccdn.';
338         } else {
339             $subdomain = '_avatars._tcp.';
340             $fallback  = 'cdn.';
341         }
342
343         // Lets try get us some records based on the choice of subdomain
344         // and the domain we had passed in.
345         $srv = dns_get_record($subdomain . $domain, DNS_SRV);
346
347         // Did we get anything? No?
348         if (count($srv) == 0) {
349             // Then let's try Libravatar.org.
350             return $fallback . 'libravatar.org';
351         }
352
353         // Sort by the priority. We must get the lowest.
354         usort($srv, array($this, 'comparePriority'));
355
356         $top = $srv[0];
357         $sum = 0;
358
359         // Try to adhere to RFC2782's weighting algorithm, page 3
360         // "arrange all SRV RRs (that have not been ordered yet) in any order,
361         // except that all those with weight 0 are placed at the beginning of
362         // the list."
363         shuffle($srv);
364         $srvs = array();
365         foreach ($srv as $s) {
366             if ($s['weight'] == 0) {
367                 array_unshift($srvs, $s);
368             } else {
369                 array_push($srvs, $s);
370             }
371         }
372
373         foreach ($srvs as $s) {
374             if ($s['pri'] == $top['pri']) {
375                 // "Compute the sum of the weights of those RRs"
376                 $sum += (int) $s['weight'];
377                 // "and with each RR associate the running sum in the selected
378                 // order."
379                 $pri[$sum] = $s;
380             }
381         }
382
383         // "Then choose a uniform random number between 0 and the sum computed
384         // (inclusive)"
385         $random = rand(0, $sum);
386
387         // "and select the RR whose running sum value is the first in the selected
388         // order which is greater than or equal to the random number selected"
389         foreach ($pri as $k => $v) {
390             if ($k >= $random) {
391                 return $v['target'];
392             }
393         }
394     }
395
396     /**
397      * Sorting function for record priorities.
398      *
399      * @param mixed $a A mixed value passed by usort()
400      * @param mixed $b A mixed value passed by usort()
401      *
402      * @return mixed The result of the comparison
403      *
404      * @since Method available since Release 0.1.0
405      */
406     protected function comparePriority($a, $b)
407     {
408         return $a['pri'] - $b['pri'];
409     }
410
411     /**
412      * Automatically set the https option depending on the current connection
413      * value.
414      *
415      * If the current connection is HTTPS, the https options is activated.
416      * If it is not HTTPS, the https option is deactivated.
417      *
418      * @return self
419      */
420     public function detectHttps()
421     {
422         $this->setHttps(
423             isset($_SERVER['HTTPS']) && $_SERVER['HTTPS']
424         );
425
426         return $this;
427     }
428
429     /**
430      * Verify and cast the email address hashing algorithm to use.
431      *
432      * @param string $algorithm Algorithm to use, "sha256" or "md5".
433      *
434      * @return string Algorithm
435      *
436      * @throws InvalidArgumentException When an unsupported algorithm is given
437      */
438     protected function processAlgorithm($algorithm)
439     {
440         $algorithm = (string)$algorithm;
441         if ($algorithm !== 'md5' && $algorithm !== 'sha256') {
442             throw new InvalidArgumentException(
443                 'Only md5 and sha256 hashing supported'
444             );
445         }
446
447         return $algorithm;
448     }
449
450     /**
451      * Verify and cast the default URL to use when no avatar image can be found.
452      * If none is set, the libravatar logo is returned.
453      *
454      * @param string $url Full URL to use OR one of the following:
455      *                    - "404" - give a "404 File not found" instead of an image
456      *                    - "mm"
457      *                    - "identicon"
458      *                    - "monsterid"
459      *                    - "wavatar"
460      *                    - "retro"
461      *
462      * @return string Default URL
463      *
464      * @throws InvalidArgumentException When an invalid URL is given
465      */
466     protected function processDefault($url)
467     {
468         if ($url === null) {
469             return $url;
470         }
471
472         $url = (string)$url;
473
474         switch ($url) {
475         case '404':
476         case 'mm':
477         case 'identicon':
478         case 'monsterid':
479         case 'wavatar':
480         case 'retro':
481             break;
482         default:
483             $valid = filter_var($url, FILTER_VALIDATE_URL);
484             if (!$valid) {
485                 throw new InvalidArgumentException('Invalid default avatar URL');
486             }
487             break;
488         }
489
490         return $url;
491     }
492
493     /**
494      * Verify and cast the required size of the images.
495      *
496      * @param integer $size Size (width and height in pixels) of the image.
497      *                      NULL for the default width.
498      *
499      * @return integer Size
500      *
501      * @throws InvalidArgumentException When a size <= 0 is given
502      */
503     protected function processSize($size)
504     {
505         if ($size === null) {
506             return $size;
507         }
508
509         $size = (int)$size;
510         if ($size <= 0) {
511             throw new InvalidArgumentException('Size has to be larger than 0');
512         }
513
514         return (int)$size;
515     }
516
517
518     /**
519      * Set the email address hashing algorithm to use.
520      * To keep gravatar compatibility, use "md5".
521      *
522      * @param string $algorithm Algorithm to use, "sha256" or "md5".
523      *
524      * @return self
525      * @throws InvalidArgumentException When an unsupported algorithm is given
526      */
527     public function setAlgorithm($algorithm)
528     {
529         $this->algorithm = $this->processAlgorithm($algorithm);
530
531         return $this;
532     }
533
534     /**
535      * Set the default URL to use when no avatar image can be found.
536      * If none is set, the gravatar logo is returned.
537      *
538      * @param string $url Full URL to use OR one of the following:
539      *                    - "404" - give a "404 File not found" instead of an image
540      *                    - "mm"
541      *                    - "identicon"
542      *                    - "monsterid"
543      *                    - "wavatar"
544      *                    - "retro"
545      *
546      * @return self
547      * @throws InvalidArgumentException When an invalid URL is given
548      */
549     public function setDefault($url)
550     {
551         $this->default = $this->processDefault($url);
552
553         return $this;
554     }
555
556     /**
557      * Set if HTTPS URLs shall be returned.
558      *
559      * @param boolean $useHttps If HTTPS url shall be returned
560      *
561      * @return self
562      *
563      * @see detectHttps()
564      */
565     public function setHttps($useHttps)
566     {
567         $this->https = (bool)$useHttps;
568
569         return $this;
570     }
571
572     /**
573      * Set the required size of the images.
574      * Every avatar image is square sized, which means you need to set only number.
575      *
576      * @param integer $size Size (width and height) of the image
577      *
578      * @return self
579      * @throws InvalidArgumentException When a size <= 0 is given
580      */
581     public function setSize($size)
582     {
583         $this->size = $this->processSize($size);
584
585         return $this;
586     }
587
588 }
589
590 /*
591  * Local variables:
592  * tab-width: 4
593  * c-basic-offset: 4
594  * c-hanging-comment-ender-p: nil
595  * End:
596  */
597
598 ?>
599