2 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
4 * PHP support for the Libravatar.org service.
10 * Copyright (c) 2011 Services_Libravatar committers.
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:
19 * The above copyright notice and this permission notice shall be included in
20 * all copies or substantial portions of the Software.
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
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
40 * PHP support for the Libravatar.org service.
42 * Using this class is easy. After including or requiring
43 * Services/Libravatar.php simply do:
45 * $libravatar = new Services_Libravatar();
46 * $url = $libravatar->getUrl('melissa@meldraweb.com');
49 * This would populate $url with the string:
51 * http://cdn.libravatar.org/avatar/4db84629c121f2d443d33bdb9fd149bc
54 * A complicated lookup using all the options is:
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'
63 * $url = $libravatar->getUrl('melissa@meldraweb.com');
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.1
72 * @link http://pear.php.net/package/Services_Libravatar
73 * @since Class available since Release 0.1.0
75 class Services_Libravatar
78 * Hashing algorithm to use
81 * @see processAlgorithm()
84 protected $algorithm = 'md5';
87 * Default image URL to use
90 * @see processDefault()
96 * If HTTPS URLs should be used
105 * Image size in pixels
115 * Composes a URL for the identifier and options passed in
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.
120 * @param string $identifier a string of either an email address
122 * @param array $options an array of (bool) https, (string) algorithm
123 * (string) size, (string) default.
124 * See the set* methods.
126 * @return string A string of a full URL for an avatar image
128 * @since Method available since Release 0.2.0
129 * @deprecated Use getUrl() instead
131 public function url($identifier, $options = array())
133 return $this->getUrl($identifier, $options);
137 * Composes a URL for the identifier and options passed in
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.
142 * @param string $identifier a string of either an email address
144 * @param array $options an array of (bool) https, (string) algorithm
145 * (string) size, (string) default.
146 * See the set* methods.
148 * @return string A string of a full URL for an avatar image
150 * @since Method available since Release 0.2.0
151 * @throws InvalidArgumentException When an invalid option is passed
153 public function getUrl($identifier, $options = array())
155 // If no identifier has been passed, set it to a null.
156 // This way, there'll always be something returned.
160 $identifier = $this->normalizeIdentifier($identifier);
164 $options = $this->checkOptionsArray($options);
165 $https = $this->https;
166 if (isset($options['https'])) {
167 $https = (bool)$options['https'];
170 $algorithm = $this->algorithm;
171 if (isset($options['algorithm'])) {
172 $algorithm = $this->processAlgorithm($options['algorithm']);
175 $default = $this->default;
176 if (isset($options['default'])) {
177 $default = $this->processDefault($options['default']);
180 if (isset($options['size'])) {
181 $size = $this->processSize($options['size']);
185 $identifierHash = $this->identifierHash($identifier, $algorithm);
187 // Get the domain so we can determine the SRV stuff for federation
188 $domain = $this->domainGet($identifier);
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';
196 if ($size !== null) {
197 $params['size'] = $size;
199 if ($default !== null) {
200 $params['default'] = $default;
203 if (count($params) > 0) {
204 $paramString = '?' . http_build_query($params);
207 // Compose the URL from the pieces we generated
208 $url = $protocol . '://' . $service . '/avatar/' . $identifierHash
211 // Return the URL string
216 * Checks the options array and verify that only allowed options are in it.
218 * @param array $options Array of options for getUrl()
221 * @throws Exception When an invalid option is used
223 protected function checkOptionsArray($options)
225 //this short options are deprecated!
226 if (isset($options['s'])) {
227 $options['size'] = $options['s'];
228 unset($options['s']);
230 if (isset($options['d'])) {
231 $options['default'] = $options['d'];
232 unset($options['d']);
235 $allowedOptions = array(
241 foreach ($options as $key => $value) {
242 if (!isset($allowedOptions[$key])) {
243 throw new InvalidArgumentException(
244 'Invalid option in array: ' . $key
253 * Normalizes the identifier (E-mail address or OpenID)
255 * @param string $identifier E-Mail address or OpenID
257 * @return string Normalized identifier
259 protected function normalizeIdentifier($identifier)
261 if (filter_var($identifier, FILTER_VALIDATE_EMAIL)) {
262 return strtolower($identifier);
264 return self::normalizeOpenId($identifier);
269 * Create a hash of the identifier.
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.
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
280 * @return string A string hash of the identifier.
282 * @since Method available since Release 0.1.0
284 protected function identifierHash($identifier, $hash = 'md5')
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);
292 //no email, so the identifier has to be an OpenID
293 return hash('sha256', $identifier);
297 * Normalizes an identifier (URI or XRI)
299 * @param mixed $identifier URI or XRI to be normalized
301 * @return string Normalized Identifier.
302 * Empty string when the OpenID is invalid.
304 * @internal Adapted from OpenID::normalizeIdentifier()
306 public static function normalizeOpenId($identifier)
309 if (preg_match('@^xri://@i', $identifier)) {
310 return preg_replace('@^xri://@i', '', $identifier);
313 if (in_array($identifier[0], array('=', '@', '+', '$', '!'))) {
318 if (!preg_match('@^http[s]?://@i', $identifier)) {
319 $identifier = 'http://' . $identifier;
321 if (strpos($identifier, '/', 8) === false) {
324 if (!filter_var($identifier, FILTER_VALIDATE_URL)) {
328 $parts = parse_url($identifier);
329 $parts['scheme'] = strtolower($parts['scheme']);
330 $parts['host'] = strtolower($parts['host']);
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']) ? '@' : '')
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'] : ''
346 . (isset($parts['query']) ? '?' . $parts['query'] : '');
347 //leave out fragment as requested by the spec
351 * Grab the domain from the identifier.
353 * Extract the domain from the Email or OpenID.
355 * @param string $identifier A string of the email address or openid URL
357 * @return string A string of the domain to use
359 * @since Method available since Release 0.1.0
361 protected function domainGet($identifier)
363 if ($identifier === null) {
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);
375 $url = parse_url($identifier);
376 $domain = $url['host'];
377 if (isset($url['port']) && $url['scheme'] === 'http'
378 && $url['port'] != 80
379 || isset($url['port']) && $url['scheme'] === 'https'
380 && $url['port'] != 443
382 $domain .= ':' . $url['port'];
389 * Get the target to use.
391 * Get the SRV record, filtered by priority and weight. If our domain
392 * has no SRV records, fall back to Libravatar.org
394 * @param string $domain A string of the domain we extracted from the
395 * provided identifier with domainGet()
396 * @param boolean $https Whether or not to look for https records
398 * @return string The target URL.
400 * @since Method available since Release 0.1.0
402 protected function srvGet($domain, $https = false)
405 // Are we going secure? Set up a fallback too.
406 if (isset($https) && $https === true) {
407 $subdomain = '_avatars-sec._tcp.';
408 $fallback = 'seccdn.';
410 $subdomain = '_avatars._tcp.';
414 // Lets try get us some records based on the choice of subdomain
415 // and the domain we had passed in.
416 $srv = @dns_get_record($subdomain . $domain, DNS_SRV);
418 // Did we get anything? No?
420 // Then let's try Libravatar.org.
421 return $fallback . 'libravatar.org';
424 // Sort by the priority. We must get the lowest.
425 usort($srv, array($this, 'comparePriority'));
430 // Try to adhere to RFC2782's weighting algorithm, page 3
431 // "arrange all SRV RRs (that have not been ordered yet) in any order,
432 // except that all those with weight 0 are placed at the beginning of
436 foreach ($srv as $s) {
437 if ($s['weight'] == 0) {
438 array_unshift($srvs, $s);
440 array_push($srvs, $s);
444 foreach ($srvs as $s) {
445 if ($s['pri'] == $top['pri']) {
446 // "Compute the sum of the weights of those RRs"
447 $sum += (int) $s['weight'];
448 // "and with each RR associate the running sum in the selected
454 // "Then choose a uniform random number between 0 and the sum computed
456 $random = rand(0, $sum);
458 // "and select the RR whose running sum value is the first in the selected
459 // order which is greater than or equal to the random number selected"
460 foreach ($pri as $k => $v) {
468 * Sorting function for record priorities.
470 * @param mixed $a A mixed value passed by usort()
471 * @param mixed $b A mixed value passed by usort()
473 * @return mixed The result of the comparison
475 * @since Method available since Release 0.1.0
477 protected function comparePriority($a, $b)
479 return $a['pri'] - $b['pri'];
483 * Automatically set the https option depending on the current connection
486 * If the current connection is HTTPS, the https options is activated.
487 * If it is not HTTPS, the https option is deactivated.
491 public function detectHttps()
494 isset($_SERVER['HTTPS']) && $_SERVER['HTTPS']
501 * Verify and cast the email address hashing algorithm to use.
503 * @param string $algorithm Algorithm to use, "sha256" or "md5".
505 * @return string Algorithm
507 * @throws InvalidArgumentException When an unsupported algorithm is given
509 protected function processAlgorithm($algorithm)
511 $algorithm = (string)$algorithm;
512 if ($algorithm !== 'md5' && $algorithm !== 'sha256') {
513 throw new InvalidArgumentException(
514 'Only md5 and sha256 hashing supported'
522 * Verify and cast the default URL to use when no avatar image can be found.
523 * If none is set, the libravatar logo is returned.
525 * @param string $url Full URL to use OR one of the following:
526 * - "404" - give a "404 File not found" instead of an image
533 * @return string Default URL
535 * @throws InvalidArgumentException When an invalid URL is given
537 protected function processDefault($url)
556 $valid = filter_var($url, FILTER_VALIDATE_URL);
558 throw new InvalidArgumentException('Invalid default avatar URL');
567 * Verify and cast the required size of the images.
569 * @param integer $size Size (width and height in pixels) of the image.
570 * NULL for the default width.
572 * @return integer Size
574 * @throws InvalidArgumentException When a size <= 0 is given
576 protected function processSize($size)
578 if ($size === null) {
584 throw new InvalidArgumentException('Size has to be larger than 0');
592 * Set the email address hashing algorithm to use.
593 * To keep gravatar compatibility, use "md5".
595 * @param string $algorithm Algorithm to use, "sha256" or "md5".
598 * @throws InvalidArgumentException When an unsupported algorithm is given
600 public function setAlgorithm($algorithm)
602 $this->algorithm = $this->processAlgorithm($algorithm);
608 * Set the default URL to use when no avatar image can be found.
609 * If none is set, the gravatar logo is returned.
611 * @param string $url Full URL to use OR one of the following:
612 * - "404" - give a "404 File not found" instead of an image
620 * @throws InvalidArgumentException When an invalid URL is given
622 public function setDefault($url)
624 $this->default = $this->processDefault($url);
630 * Set if HTTPS URLs shall be returned.
632 * @param boolean $useHttps If HTTPS url shall be returned
638 public function setHttps($useHttps)
640 $this->https = (bool)$useHttps;
646 * Set the required size of the images.
647 * Every avatar image is square sized, which means you need to set only number.
649 * @param integer $size Size (width and height) of the image
652 * @throws InvalidArgumentException When a size <= 0 is given
654 public function setSize($size)
656 $this->size = $this->processSize($size);
667 * c-hanging-comment-ender-p: nil