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