]> git.mxchange.org Git - friendica-addons.git/blob - libravatar/Services/Libravatar.php
Move StatusNet addon dependencies in own library subfolder
[friendica-addons.git] / libravatar / Services / 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.1
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         $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
381         ) {
382             $domain .= ':' . $url['port'];
383         }
384
385         return $domain;
386     }
387
388     /**
389      * Get the target to use.
390      *
391      * Get the SRV record, filtered by priority and weight. If our domain
392      * has no SRV records, fall back to Libravatar.org
393      *
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
397      *
398      * @return string The target URL.
399      *
400      * @since Method available since Release 0.1.0
401      */
402     protected function srvGet($domain, $https = false)
403     {
404
405         // Are we going secure? Set up a fallback too.
406         if (isset($https) && $https === true) {
407             $subdomain = '_avatars-sec._tcp.';
408             $fallback  = 'seccdn.';
409         } else {
410             $subdomain = '_avatars._tcp.';
411             $fallback  = 'cdn.';
412         }
413
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);
417
418         // Did we get anything? No?
419         if (count($srv) == 0) {
420             // Then let's try Libravatar.org.
421             return $fallback . 'libravatar.org';
422         }
423
424         // Sort by the priority. We must get the lowest.
425         usort($srv, array($this, 'comparePriority'));
426
427         $top = $srv[0];
428         $sum = 0;
429
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
433         // the list."
434         shuffle($srv);
435         $srvs = array();
436         foreach ($srv as $s) {
437             if ($s['weight'] == 0) {
438                 array_unshift($srvs, $s);
439             } else {
440                 array_push($srvs, $s);
441             }
442         }
443
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
449                 // order."
450                 $pri[$sum] = $s;
451             }
452         }
453
454         // "Then choose a uniform random number between 0 and the sum computed
455         // (inclusive)"
456         $random = rand(0, $sum);
457
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) {
461             if ($k >= $random) {
462                 return $v['target'];
463             }
464         }
465     }
466
467     /**
468      * Sorting function for record priorities.
469      *
470      * @param mixed $a A mixed value passed by usort()
471      * @param mixed $b A mixed value passed by usort()
472      *
473      * @return mixed The result of the comparison
474      *
475      * @since Method available since Release 0.1.0
476      */
477     protected function comparePriority($a, $b)
478     {
479         return $a['pri'] - $b['pri'];
480     }
481
482     /**
483      * Automatically set the https option depending on the current connection
484      * value.
485      *
486      * If the current connection is HTTPS, the https options is activated.
487      * If it is not HTTPS, the https option is deactivated.
488      *
489      * @return self
490      */
491     public function detectHttps()
492     {
493         $this->setHttps(
494             isset($_SERVER['HTTPS']) && $_SERVER['HTTPS']
495         );
496
497         return $this;
498     }
499
500     /**
501      * Verify and cast the email address hashing algorithm to use.
502      *
503      * @param string $algorithm Algorithm to use, "sha256" or "md5".
504      *
505      * @return string Algorithm
506      *
507      * @throws InvalidArgumentException When an unsupported algorithm is given
508      */
509     protected function processAlgorithm($algorithm)
510     {
511         $algorithm = (string)$algorithm;
512         if ($algorithm !== 'md5' && $algorithm !== 'sha256') {
513             throw new InvalidArgumentException(
514                 'Only md5 and sha256 hashing supported'
515             );
516         }
517
518         return $algorithm;
519     }
520
521     /**
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.
524      *
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
527      *                    - "mm"
528      *                    - "identicon"
529      *                    - "monsterid"
530      *                    - "wavatar"
531      *                    - "retro"
532      *
533      * @return string Default URL
534      *
535      * @throws InvalidArgumentException When an invalid URL is given
536      */
537     protected function processDefault($url)
538     {
539         if ($url === null) {
540             return $url;
541         }
542
543         $url = (string)$url;
544
545         switch ($url) {
546         case '404':
547         case 'mm':
548         case 'identicon':
549         case 'monsterid':
550         case 'wavatar':
551         case 'retro':
552             break;
553         default:
554             $valid = filter_var($url, FILTER_VALIDATE_URL);
555             if (!$valid) {
556                 throw new InvalidArgumentException('Invalid default avatar URL');
557             }
558             break;
559         }
560
561         return $url;
562     }
563
564     /**
565      * Verify and cast the required size of the images.
566      *
567      * @param integer $size Size (width and height in pixels) of the image.
568      *                      NULL for the default width.
569      *
570      * @return integer Size
571      *
572      * @throws InvalidArgumentException When a size <= 0 is given
573      */
574     protected function processSize($size)
575     {
576         if ($size === null) {
577             return $size;
578         }
579
580         $size = (int)$size;
581         if ($size <= 0) {
582             throw new InvalidArgumentException('Size has to be larger than 0');
583         }
584
585         return (int)$size;
586     }
587
588
589     /**
590      * Set the email address hashing algorithm to use.
591      * To keep gravatar compatibility, use "md5".
592      *
593      * @param string $algorithm Algorithm to use, "sha256" or "md5".
594      *
595      * @return self
596      * @throws InvalidArgumentException When an unsupported algorithm is given
597      */
598     public function setAlgorithm($algorithm)
599     {
600         $this->algorithm = $this->processAlgorithm($algorithm);
601
602         return $this;
603     }
604
605     /**
606      * Set the default URL to use when no avatar image can be found.
607      * If none is set, the gravatar logo is returned.
608      *
609      * @param string $url Full URL to use OR one of the following:
610      *                    - "404" - give a "404 File not found" instead of an image
611      *                    - "mm"
612      *                    - "identicon"
613      *                    - "monsterid"
614      *                    - "wavatar"
615      *                    - "retro"
616      *
617      * @return self
618      * @throws InvalidArgumentException When an invalid URL is given
619      */
620     public function setDefault($url)
621     {
622         $this->default = $this->processDefault($url);
623
624         return $this;
625     }
626
627     /**
628      * Set if HTTPS URLs shall be returned.
629      *
630      * @param boolean $useHttps If HTTPS url shall be returned
631      *
632      * @return self
633      *
634      * @see detectHttps()
635      */
636     public function setHttps($useHttps)
637     {
638         $this->https = (bool)$useHttps;
639
640         return $this;
641     }
642
643     /**
644      * Set the required size of the images.
645      * Every avatar image is square sized, which means you need to set only number.
646      *
647      * @param integer $size Size (width and height) of the image
648      *
649      * @return self
650      * @throws InvalidArgumentException When a size <= 0 is given
651      */
652     public function setSize($size)
653     {
654         $this->size = $this->processSize($size);
655
656         return $this;
657     }
658
659 }
660
661 /*
662  * Local variables:
663  * tab-width: 4
664  * c-basic-offset: 4
665  * c-hanging-comment-ender-p: nil
666  * End:
667  */
668
669 ?>