<?php

if (IN_serendipity !== true) {
    die ("Don't hack!");
}

// Load possible language files.
@serendipity_plugin_api::load_language(dirname(__FILE__));

// Actual version of this plugin
@define('PLUGIN_EVENT_GRAVATAR_VERSION', '2.0.1');

// Defines the maximum available method  slots in the configuration.
@define('PLUGIN_EVENT_GRAVATAR_METHOD_MAX', 6);

// Switch on and off debugging mode of the plugin
@define('PLUGIN_EVENT_GRAVATAR_DEBUG', false);

class serendipity_event_gravatar extends serendipity_event
{
    var $title = PLUGIN_EVENT_GRAVATAR_NAME;

    var $cache_dir                  = null;
    var $defaultImageConfiguration  = null;
    var $defaultImageConfigurationdefault = null;

    var $avatarConfiguration        = array();
    var $cache_seconds              = 0;

    function introspect(&$propbag)
    {
        global $serendipity;

        $propbag->add('name',          PLUGIN_EVENT_GRAVATAR_NAME);
        $propbag->add('description',   PLUGIN_EVENT_GRAVATAR_DESC);
        $propbag->add('stackable',     false);
        $propbag->add('author',        'Garvin Hicking, Grischa Brockhaus');
        $propbag->add('requirements',  array(
            'serendipity' => '1.6',
            'smarty'      => '2.6.7',
            'php'         => '8.0'
        ));
        $propbag->add('version',       PLUGIN_EVENT_GRAVATAR_VERSION);
        $propbag->add('groups',        array('IMAGES'));
        $propbag->add('copyright',     'GPLv2 (or later)');
        $propbag->add('event_hooks',   array(
            'frontend_display' => true,
            'frontend_comment' => true,
            'external_plugin'  => true,
            'css'              => true,
        ));

        $propbag->add('legal',    array(
            'services' => array(
                'gravatar' => array(
                    'url'  => 'https://gravatar.com',
                    'desc' => 'Transmits comment data to retrieve unique avatar for a user.'
                ),
                'favatar' => array(
                    'url'  => 'http://www.peej.co.uk/projects/favatars.html',
                    'desc' => 'Transmits comment data to retrieve unique avatar for a user.'
                ),
                'pavatar' => array(
                    'url'  => 'http://www.pavatar.com',
                    'desc' => 'Transmits comment data to retrieve unique avatar for a user.'
                ),
                'monsterid' => array(
                    'url'  => 'http://www.splitbrain.org/go/monsterid',
                    'desc' => 'Transmits comment data to retrieve unique avatar for a user.'
                ),
                'identicon' => array(
                    'url'  => 'http://scott.sherrillmix.com/blog/blogger/wp_identicon/',
                    'desc' => 'Transmits comment data to retrieve unique avatar for a user.'
                ),
                'wavatars' => array(
                    'url'  => 'http://www.shamusyoung.com/twentysidedtale/?p=1462',
                    'desc' => 'Transmits comment data to retrieve unique avatar for a user.'
                ),

            ),
            'frontend' => array(
                'To display unique avatar images for blog comments, data specific to the correspondig service is transmitted to retrieve the proper avatar.',
            ),
            'backend' => array(
            ),
            'cookies' => array(
            ),
            'stores_user_input'     => true,
            'stores_ip'             => false,
            'uses_ip'               => true,
            'transmits_user_input'  => true
        ));


        $configuration =               array('longdescription', 'separator');
        $config_methods =              array();
        for ($idx=1; $idx<=PLUGIN_EVENT_GRAVATAR_METHOD_MAX; $idx++) {
            $config_methods[] = "method_$idx";
        }

        $propbag->add('configuration',
            array_merge($configuration, $config_methods,
                array('defaultavatar', 'recent_entries', 'infoline',
                    'autoralt', 'smartyimage', 'align', 'size', 'cache', 'rating',
                    'gravatar_fallback','gravatar_fallback_use_always','warning')
            )
        );
    }

    function introspect_config_item($name, &$propbag)
    {
        global $serendipity;

        $types = array(
            'gravatar'  => "Gravatar",
            'favatar'   => "Favatar",
            'pavatar'   => "Pavatar",
            'monsterid' => "Monster ID",
            'wavatars'  => "Wavatars",
            'identicon' => "Identicon/YCon",
            'default'   => PLUGIN_EVENT_GRAVATAR_METHOD_DEFAULT,
            'none'      => "---",
        );

        // Add config for methods.
        for ($idx=1; $idx<=PLUGIN_EVENT_GRAVATAR_METHOD_MAX; $idx++) {
            if ($name=="method_$idx") {
                $propbag->add('type',       'select');
                $propbag->add('name',       "($idx) " . PLUGIN_EVENT_GRAVATAR_METHOD);
                $propbag->add('description',PLUGIN_EVENT_GRAVATAR_METHOD_DESC);
                $propbag->add('select_values', $types);
                $propbag->add('default',     'pavatar');
                return true;
            }
        }

        $gravatar_fallbacks = array(
            'monsterid' => "Monster ID",
            'wavatar'   => "Wavatar",
            'identicon' => "Identicon",
            'default'   => "Gravatar symbol",
            'none'      => "---",
        );

        switch($name) {

            case 'smartyimage':
                $propbag->add('type',        'boolean');
                $propbag->add('name',        PLUGIN_EVENT_GRAVATAR_USE_SMARTY);
                $propbag->add('description', PLUGIN_EVENT_GRAVATAR_USE_SMARTY_DESC);
                $propbag->add('default',     false);
                break;

            case 'infoline':
                $propbag->add('type',        'boolean');
                $propbag->add('name',        PLUGIN_EVENT_GRAVATAR_INFOLINE);
                $propbag->add('description', PLUGIN_EVENT_GRAVATAR_INFOLINE_DESC);
                $propbag->add('default',     true);
                break;

            case 'recent_entries':
                $propbag->add('type',        'boolean');
                $propbag->add('name',        PLUGIN_EVENT_GRAVATAR_RECENT_ENTRIES);
                $propbag->add('description', PLUGIN_EVENT_GRAVATAR_RECENT_ENTRIES_DESC);
                $propbag->add('default',     true);
                break;

            case 'warning':
                $propbag->add('type',        'content');
                $propbag->add('default',     PLUGIN_EVENT_GRAVATAR_EXTLING_WARNING);
                break;

            case 'longdescription':
                $propbag->add('type',        'content');
                $propbag->add('default',     PLUGIN_EVENT_GRAVATAR_LONG_DESCRIPTION);
                break;

            case 'separator':
                $propbag->add('type',        'separator');
                break;

            case 'gravatar_fallback':
                $propbag->add('type',       'select');
                $propbag->add('name',       PLUGIN_EVENT_GRAVATAR_FALLBACK);
                $propbag->add('description',PLUGIN_EVENT_GRAVATAR_FALLBACK_DESC);
                $propbag->add('select_values', $gravatar_fallbacks);
                $propbag->add('default',     'none');
                break;

            case 'gravatar_fallback_use_always':
                $propbag->add('type',        'boolean');
                $propbag->add('name',       PLUGIN_EVENT_GRAVATAR_FALLBACK_ALLWAYS);
                $propbag->add('description',PLUGIN_EVENT_GRAVATAR_FALLBACK_ALLWAYS_DESC);
                $propbag->add('default',     false);
                break;

            case 'defaultavatar':
                $propbag->add('type',        'string');
                $propbag->add('name',        PLUGIN_EVENT_GRAVATAR_DEFAULTAVATAR);
                $propbag->add('description', PLUGIN_EVENT_GRAVATAR_DEFAULTAVATAR_DESC);
                $propbag->add('default',     '');
                break;

            case 'cache':
                $propbag->add('type',        'string');
                $propbag->add('name',        PLUGIN_EVENT_GRAVATAR_CACHING);
                $propbag->add('description', PLUGIN_EVENT_GRAVATAR_CACHING_DESC);
                $propbag->add('default',     48);
                break;

            case 'size':
                $propbag->add('type',        'string');
                $propbag->add('name',        PLUGIN_EVENT_GRAVATAR_SIZE);
                $propbag->add('description', PLUGIN_EVENT_GRAVATAR_SIZE_DESC);
                $propbag->add('default',     '40');
                break;

            case 'border':
                $propbag->add('type',        'string');
                $propbag->add('name',        PLUGIN_EVENT_GRAVATAR_BORDER);
                $propbag->add('description', PLUGIN_EVENT_GRAVATAR_BORDER_DESC);
                $propbag->add('default',     '');
                break;

            case 'rating':
                $propbag->add('type',        'radio');
                $propbag->add('name',        PLUGIN_EVENT_GRAVATAR_RATING);
                $propbag->add('description', PLUGIN_EVENT_GRAVATAR_RATING_DESC);
                $propbag->add('radio',       array(
                    'value' => array('-', 'G', 'PG', 'R', 'X'),
                    'desc'  => array(PLUGIN_EVENT_GRAVATAR_RATING_NO,PLUGIN_EVENT_GRAVATAR_RATING_G, PLUGIN_EVENT_GRAVATAR_RATING_PG, PLUGIN_EVENT_GRAVATAR_RATING_R, PLUGIN_EVENT_GRAVATAR_RATING_X)
                ));
                $propbag->add('radio_per_row', '1');
                $propbag->add('default',     '-');
                break;

            case 'align':
                $align = array(
                    'left'      => PLUGIN_EVENT_GRAVATAR_ALIGN_LEFT,
                    'right'     => PLUGIN_EVENT_GRAVATAR_ALIGN_RIGHT,
                    'noalign'   => PLUGIN_EVENT_GRAVATAR_ALIGN_NONE,
                );

                $propbag->add('type',           'select');
                $propbag->add('name',           PLUGIN_EVENT_GRAVATAR_ALIGN);
                $propbag->add('description',    PLUGIN_EVENT_GRAVATAR_ALIGN_DESC);
                $propbag->add('select_values',  $align);
                $propbag->add('default',        'right');
                break;

            case 'autoralt':
                $propbag->add('type',        'boolean');
                $propbag->add('name',        PLUGIN_EVENT_GRAVATAR_AUTOR_ALT);
                $propbag->add('description', PLUGIN_EVENT_GRAVATAR_AUTOR_ALT_DESC);
                $propbag->add('default',     false);
                break;

            default:
                return false;
        }
        return true;
    }

    function generate_content(&$title)
    {
        $title = PLUGIN_EVENT_GRAVATAR_NAME;
    }

    /**
     *  Will be called while saving settings
     */
    function cleanup()
    {
        // *Always* clean up the cache after changing configuration, else
        // the configuration change will first be seen after cache time is run out.
        $this->log("-------");
        $cacheDir = $this->getCacheDirectory();
        if (is_dir($cacheDir) && $handle = opendir($cacheDir)) {
            while (false !== ($file = readdir($handle))) {
                $filename = $cacheDir . '/' . $file;
                if (!is_dir($filename)) {
                    $this->log("CLEANUP CACHE: " . $filename);
                    unlink($filename);
                }
            }
        }
    }

    function event_hook($event, &$bag, &$eventData, $addData = null)
    {
        global $serendipity;
        static $cache = null;
        static $method = null;

        $hooks = &$bag->get('event_hooks');

        if ($cache === null) {
            $cache = $this->get_config('cache') * 60 * 60; // convert hours to seconds
            $this ->cache_seconds = $cache;
        }

        if ($method === null) {
            $method = $this->get_config('method', 'gravatar');
        }

        if (isset($hooks[$event])) {
            switch($event) {

                // Catch external_plugin event for fresh fetching avatar icons
                // This will response with an image (not with html code)
                case 'external_plugin':
                    $parts      = explode('_', $eventData);
                    if (count($parts)<4) {
                        return false;
                    }
                    if ($parts[0] == 'fetchAvatar') {
                        if (count($parts)!=5) return false;
                        $info = array();
                        $info['url']       = $this->urldecode($parts[1]);
                        $info['email_md5'] = $parts[2];
                        $info['author']    = $this->urldecode($parts[3]);
                        $info['cid']       = $parts[4];
                        $this->log("-------");
                        $this->log("fetch Avatar: " . urldecode($parts[1]));
                        $this->fetchAvatar($info);
                        return true;
                    } else if ($parts[0] == 'cachedAvatar') {
                        if (count($parts)!=4) return false;
                        $cache_file = $this->getCacheDirectory() . '/' . $parts[1] .'_' .$parts[2] . '_' .$parts[3];
                        $lastrun = $cache_file . '.lastrun';
                        $this->log("-------");
                        $this->log("show cached Avatar: $cache_file");
                        // Get last run information
                        if (file_exists($lastrun)){
                            $fp = fopen($lastrun, 'rb');
                            $this->avatarConfiguration = unserialize(fread($fp, filesize($lastrun)));
                            fclose($fp);
                        }
                        $this->show($cache_file);
                        return true;
                    } else {
                        return false;
                    }
                    break;

                // Print out image html for the user avatar into the frontend_display
                case 'frontend_display':
                    if (!isset($eventData['comment'])) {
                        return true;
                    }

                    $this->printAvatarHtml($eventData, $addData);
                    break;

                case 'css':
                    // avatar css has to be emitted no matter of smarty enabled: the sidebar needs it.
                    // CSS class does NOT exist by user customized template styles, include default
                    if (false === (strpos($eventData, '.avatar_left') || strpos($eventData, '.avatar_rigth'))) {
                        $eventData .= '

/* serendipity_event_gravatar start */

.avatar_left {
    float:left;
    margin-left:0px;
    margin-right:10px;
}
.avatar_right {
    float:right;
    margin-right:0px;
    margin-left:10px;
}

/* serendipity_event_gravatar end */

';

                    }
                    break;

                // Adds information about the actual supported avatar types below the comment input
                case 'frontend_comment':

                    // Suppress infoline about configured avatar types if configured like that:
                    if (!serendipity_db_bool($this->get_config('infoline', 'true'))){
                        return false;
                    }

                    // The contact form uses the comments, too. We don't want this information line there and detect it by the missing properties entry.
                    if (empty($eventData['properties'])){
                        return false;
                    }
                    $supported_methods = '';
                    for($methodnr = 1; $methodnr <= PLUGIN_EVENT_GRAVATAR_METHOD_MAX; $methodnr++){
                        $act_method = $this->get_config("method_".$methodnr);
                        switch ($act_method){
                            case 'gravatar':
                                $supported_methods .= (empty($supported_methods) ? '' : ', ') . '<a href="https://www.gravatar.com">Gravatar</a>';
                                break;
                            case 'favatar':
                                $supported_methods .= (empty($supported_methods) ? '' : ', ') . 'Favatar (Favicons)';
                                break;
                            case 'pavatar':
                                $supported_methods .= (empty($supported_methods) ? '' : ', ') . '<a href="http://pavatar.github.io/pavatar/">Pavatar</a>';
                                break;
                            case 'monsterid':
                                $supported_methods .= (empty($supported_methods) ? '' : ', ') . '<a href="https://www.splitbrain.org/go/monsterid">Monster ID</a>';
                                break;
                            case 'identicon':
                                $supported_methods .= (empty($supported_methods) ? '' : ', ') . '<a href="http://scott.sherrillmix.com/blog/blogger/wp_identicon/">Identicon/Ycon</a>';
                                break;
                            case 'wavatars':
                                $supported_methods .= (empty($supported_methods) ? '' : ', ') . '<a href="http://www.shamusyoung.com/twentysidedtale/?p=1462">Wavatars</a>';
                                break;
                        }
                    }
                    echo '<div class="serendipity_commentDirection serendipity_comment_gravatar">' . sprintf(PLUGIN_EVENT_GRAVATAR_SUPPORTED, $supported_methods) . '</div>';
                    break;

              default:
                return false;

            }
            return true;
        } else {
            return false;
        }
    }

    // Hash the given value in the way expected for the gravatar fetch. The same hash
    // can be used for contructing the cached urls
    function pluginHash($value) {
        return hash('sha256', trim(strtolower($value)));
    }

    /**
     * Returns HTML displaying the user avatar. This is done without any call to external servers.
     * If a cached avatar is found, the image will have it as SRC, else the SRC will be filled with
     * an external_plugin call, that will try to fetch a fresh avatar later.
     */
    function printAvatarHtml(&$eventData, &$addData)
    {
        global $serendipity;

        $useSmarty = serendipity_db_bool($this->get_config('smartyimage', 'false'));

        // comments sidebar plugin doesn't support smarty, so switch it off, if detected
        if (is_array($addData) && $addData['from'] == 'serendipity_plugin_comments:generate_content') {
           if (!serendipity_db_bool($this->get_config('recent_entries', 'true'))) {
               return false;
           }
           $useSmarty = false;
        }

        if (empty($eventData['url']) && empty($eventData['email']) && empty($eventData['author']) && !$this->supportDefaultAvatar()) {
            $this->log("No url nor email handed and default avatar not supported!");
            return false;
        }

        if (!empty($eventData['url']) && !preg_match('@^https*://@i',$eventData['url'])){
            $this->log("Changed wrong url: {$eventData['url']}");
            $eventData['url'] = 'http://' . $eventData['url'];
        }

        $url = '';
        if (!empty($eventData['url'])) {
            $url = $eventData['url'];
        }

        if (!empty($eventData['url'])) { // Strip Query params
            $urlparts = explode('?', $eventData['url']);
            $url = $urlparts[0];
        }
        $title = '';
        $author = 'unknown';
        if (isset($eventData['author'])) {
            $author = (function_exists('serendipity_specialchars') ? serendipity_specialchars($eventData['author']) : htmlspecialchars($eventData['author'], ENT_COMPAT, LANG_CHARSET));
            $title = $author;
        }

        if (isset($eventData['email']) && !empty($eventData['email'])) {
            $email_md5 = $this->pluginHash($eventData['email']);
        }
        else {
            $email_md5 = '';
        }
        if ($this->cache_seconds > 0) {
            $cache_file = $this->getCacheFilePath($eventData);
            // if no cache filename was generated, no usable user data was found.
            // this means: it won't be possible to generate any image, so break at this point.
            if (!isset($cache_file)) {
                return false;
            }
            $this->log("comment print: " . print_r($eventData, true));
            // If there is a cache file that's new enough, return the image immediately
            if (file_exists($cache_file) &&  (time() - filemtime($cache_file) < $this->cache_seconds)) {
                $url = $serendipity['baseURL'] . $serendipity['indexFile'] . '?/'
                    . $this->getPermaPluginPath() . '/cachedAvatar_' . $this->pluginHash($url) . '_' . $email_md5
                    . '_' . $this->pluginHash($author);
            } else { // no image cached yet, call external plugin hook for fetching a new one
                if (file_exists($cache_file)) {
                    // On modern servers the filetime might not be updated. Deleting and recreating
                    // it after a cache timeout will make the cache logic work again.
                    unlink($cache_file);
                }
                $url = $serendipity['baseURL'] . $serendipity['indexFile'] . '?/'
                    . $this->getPermaPluginPath() . '/fetchAvatar_' . $this->urlencode($url) . '_' . $email_md5
                    . '_' . $this->urlencode($author) . '_' . $eventData['id'];
            }

        } else { // call external plugin hook for fetching a new one
            $url = $serendipity['baseURL'] . $serendipity['indexFile'] . '?/'
                . $this->getPermaPluginPath() . '/fetchAvatar_' . $this->urlencode($url) . '_' . $email_md5
                . '_' . $this->urlencode($author) . '_' . $eventData['id'];
        }

        $image_html = $this->generateImageHtml($url, $title, $this->get_config('align', 'right'), !$useSmarty, $this->generateAvatarCssClass($addData));
        if ($useSmarty) {
            $eventData['avatar'] = $image_html;
        }
        else {
            $eventData['comment'] = $image_html . $eventData['comment'];
        }

        return true;
    }

    /**
     * Generates a CSS class for the avatar depending where it is displayed.
     * Defaults to comment_avatar in comments and is unique for plugins.
     */
    function generateAvatarCssClass($addData)
    {
        if (empty($addData)) {
            return "avatar";
        }
        $from = $addData['from'];
        $parts = explode(':',$from);
        $css = $parts[0];
        return ($css == 'functions_entries'? 'comment' : $css ) . '_avatar';
    }

    /**
     * Tests wether the default avatar is supported
     */
    function supportDefaultAvatar()
    {

        // Check if a default avatar is defined
        $default = $this->getDefaultImageConfiguration();
        if (empty($default['defaultavatar'])) {
            return false;
        }

        // check if default avatar method is configured as one of the avatar methods.
        for($methodnr = 1; $methodnr <= PLUGIN_EVENT_GRAVATAR_METHOD_MAX; $methodnr++){
            $method = $this->get_config("method_" . $methodnr);

            // if none is configured, ignore later methods!
            if ($method == 'none'){
                return false;
            }

            // return true if default avatar method is found
            if ($method == 'default'){
                return true;
            }
        }
        return false;
    }

    /**
     * Will try to fetch a fresh avatar image by user configuration. If retreiving was successfull,
     * the image will cached and displayed as binary image response.
     */
    function fetchAvatar(&$eventData)
    {
        global $serendipity;

        $methodnr = 1;

        // Assure existance of cache directory
        if (! file_exists($this->getCacheDirectory())) {
            mkdir($this->getCacheDirectory());
        }
        $default = $this->getDefaultImageConfiguration();

        // load configuration of last run
        $lastrun_fname = $this->getCacheFilePath($eventData) . '.lastrun';
        if (file_exists($lastrun_fname) && (time() - filemtime($lastrun_fname))< $this->cache_seconds) {
            $this->log("Found fresh lastrun: $lastrun_fname");
            $fp = fopen($lastrun_fname, 'rb');
            $this->avatarConfiguration = unserialize(fread($fp, filesize($lastrun_fname)));
            fclose($fp);
            $this->avatarConfiguration['loaded_from_cache'] = true;
            // go to last successfull methodnr:
            if (isset($this->avatarConfiguration['methodnr'])) {
                $methodnr = $this->avatarConfiguration['methodnr'];
                $this->log("MethodNr by lastrun: $methodnr");
            }
        }

        $success = false;
        while (!$success && $methodnr <= PLUGIN_EVENT_GRAVATAR_METHOD_MAX) {
            $method = $this->get_config("method_" . $methodnr);
            switch ($method){
                case 'gravatar':
                    $success = $this->fetchGravatar($eventData);
                    break;
                case 'favatar':
                    $success = $this->fetchFavatar($eventData);
                    break;
                case 'pavatar':
                    $success = $this->fetchPavatar($eventData);
                    break;
                case 'monsterid':
                    $success = $this->fetchMonster($eventData);
                    break;
                case 'wavatars':
                    $success = $this->fetchWavatar($eventData);
                    break;
                case 'identicon':
                    $success = $this->fetchYcon($eventData);
                    break;
                case 'default':
                    $success = $this->fetchDefault();
                    break;
                case 'none':
                    $success = true;
                    break;
            }
            if ($success){
                $this->log("fetchAvater success.");
                $this->avatarConfiguration['methodnr'] = $methodnr;
                $this->avatarConfiguration['fetch_date'] = time();
                // save configuration of this check, if it was not loaded from cache.
                if (!isset($this->avatarConfiguration['loaded_from_cache'])) {
                    $fp = fopen($lastrun_fname, 'wb');
                    fwrite($fp,serialize($this->avatarConfiguration));
                    fclose($fp);
                }
                return true;
            }
            $methodnr++;
        }
    }

    /**
     * Fetches a Gravatar and returns it as a binary image response.
     *
     * @param array eventdata the data given by the event
     * @param int cache hours for fetching images from cache
     *
     * @return boolean true, if Avatar was found and added to the comment buffer
     */
    function fetchGravatar(&$eventData)
    {
        global $serendipity;

        $this->log("Gravatar: url=" . $eventData['url'] . " email_md5=" . $eventData['email_md5'] . " author=" .$eventData['author']) ;
        // Was lastrun successfull?
        if (isset($this->avatarConfiguration['gravatar_found']) && !$this->avatarConfiguration['gravatar_found']) {
            return false;
        }
        if (empty($eventData['email_md5'])) {
            if (!serendipity_db_bool($this->get_config('gravatar_fallback_use_always', 'false')) || (empty($eventData['url']) && empty($eventData['author']))) {
                return false;
            }
            else {
                if (empty($eventData['url'])) $email_md5 = $this->pluginHash($eventData['author']);
                else $email_md5 = $this->pluginHash($eventData['url']);
            }
        }
        else {
            $email_md5 = $eventData['email_md5'];
        }
        $default = $this->getDefaultImageConfiguration();

        $gravatar_fallback = $this->get_config("gravatar_fallback");
        $fallback = "";
        if ($gravatar_fallback != 'none') {
            $fallback = '?d=' . $gravatar_fallback;
        }
        else {
            $defaultavatar = urlencode($serendipity['serendipityHTTPPath'] . 'dummy456.gif123'); // Add a not existing image to produce an error we can check
            $fallback = '?d=' . $defaultavatar;
        }
        
        $urltpl = 'https://www.gravatar.com/avatar/'
            . $email_md5
            . $fallback
            . '&size='        . $default['size']
            . ($default['rating']=='-'?'':'&rating='. $default['rating']);
        
        $this->log("Gravatar Link: " . $urltpl) ;
        $success = $this->saveAndResponseAvatar($eventData, $urltpl, 1);
        $this->avatarConfiguration['gravatar_found'] = $success;

        return $success;
    }

    /**
     * Fetch the favicon from the commenter's url
     *
     * @param array eventdata the data given by the event
     *
     * @return boolean true, if Avatar was found and added to the comment buffer
     * */
    function fetchFavatar($eventData) {
        if (! isset($eventData['url']) || empty($eventData['url'])) {
            return false;
        }
        $userpage = serendipity_request_url($eventData['url']);
        preg_match('/<link[^>]+rel="(?:shortcut )?icon"[^>]+?href="([^"]+?)"/si', $userpage, $matches);
        if (isset($matches[1])) {
            // we found a linked favicon
            $favicon = $matches[1];
            if (! str_starts_with($favicon, 'http')) {
                // If the favicon is given as a relative url, we have to construct the abolute
                $urlParts   = parse_url($eventData['url']);
                $faviconURL = $urlParts['scheme'] . '://' . $urlParts['host'] . "/$favicon";
            } else {
                $faviconURL = $favicon;
            }
            return $this->saveAndResponseAvatar($eventData, $faviconURL);
        } else {
            $urlParts   = parse_url($eventData['url']);
            $faviconURL = $urlParts['scheme'] . '://' . $urlParts['host'] . '/favicon.ico';
            return $this->saveAndResponseAvatar($eventData, $faviconURL);
        }

        return false;
    }

    /**
     * Fetch an image from the given url if it is decalred by the X-Pavatar header, linked by
     * <link rel="pavatar" href="URL"> or stored as /pavatar.png as the webroot. See
     * https://github.com/pavatar/pavatar/blob/master/Specification.md 
     *
     * @param array eventdata the data given by the event
     *
     * @return boolean true, if Avatar was found and added to the comment buffer
     * */
    function fetchPavatar($eventData) {
        global $serendipity;
        if (! isset($eventData['url']) || empty($eventData['url'])) {
            return false;
        }
        
        $userpage = serendipity_request_url($eventData['url']);
        $header = $serendipity['last_http_request']['header'];

        if (isset($header['X-Pavatar'])) {
            return;
            // Note that according to the spec, header content has to be the absolute url. We do not
            // need to work with relative urls
            return $this->saveAndResponseAvatar($eventData, $header['X-Pavatar']);
        }

        preg_match('/<link[^>]+rel="pavatar"[^>]+?href="([^"]+?)"/si', $userpage, $matches);
        if (isset($matches[1])) {
            // Again, the spec asks for an absolute URL here.
            return $this->saveAndResponseAvatar($eventData, $matches[1]);
        }

        // If neither header nor link is set we can still guess
        $urlParts   = parse_url($eventData['url']);
        $pavatarURL = $urlParts['scheme'] . '://' . $urlParts['host'] . '/pavatar.png';

        // If the pavatar does not exist this will properly return false
        return $this->saveAndResponseAvatar($eventData, $pavatarURL);
    }

    /**
     * Tries to add a favatar or pavatar (depending on the given mode) to the comment.
     *
     * @param array eventdata the data given by the event
     * @param int cache hours for fetching images from cache
     * @param string mode has to be 'P' for Pavatar or 'F' for Favatar loading.
     *
     * @return boolean true, if Avatar was found and added to the comment buffer
     */
    function fetchPFavatar(&$eventData, $mode="F")
    {
        require_once S9Y_PEAR_PATH . 'HTTP/Request2.php';
        global $serendipity;

        $default = $this->getDefaultImageConfiguration();

        $url        = $eventData['url'];
        if (empty($url)) {
            return false;
        }
        $favicon = false;

        $this->log($mode . " - Trying to fetch for $url");

        // Try to get the URL
        $parts = @parse_url($url);
        if (!is_array($parts)) {
            return false;
        }

        $ip = @gethostbyname($parts['host']);
        if (!$ip || $ip == $parts['host']) {
            return false;
        }

        $this->log($mode . " - URL ok.");

        $cache_file = $this->getCacheFilePath($eventData);

        // Load icon url detected in last run
        if (isset($this->avatarConfiguration['img_url_'.$mode])){
            $favicon = $this->avatarConfiguration['img_url_'.$mode];
            $this->log($mode . " - using last run url: $favicon");
        }

        if ($favicon === false) {
            // use optimization for localhost
            $islocalhost = ($_SERVER['HTTP_HOST'] == $parts['host']);

            if (function_exists('serendipity_request_start')) {
                serendipity_request_start();
            }

            // Evaluate URL of P/Favatar
            $options = array('follow_redirects' => true, 'max_redirects' => 3);
            if (version_compare(PHP_VERSION, '5.6.0', '<')) {
                // On earlier PHP versions, the certificate validation fails. We deactivate it on them to restore the functionality we had with HTTP/Request1
                $options['ssl_verify_peer'] = false;
            }
            $req = new HTTP_Request2($url, HTTP_Request2::METHOD_GET, $options);
            $favicon = false;
            // code 200: OK, code 30x: REDIRECTION
            $responses = "/(200 OK)|(30[0-9] Found)/"; // |(30[0-9] Moved)
            try {
                $response = $req->send();
                $pavatarHeaderIcon = $response->getHeader("X-Pavatar");
                $fContent = $response->getBody();
                if ($mode=='P' && !empty($pavatarHeaderIcon)){
                    $faviconURL = $pavatarHeaderIcon;
                    $this->log("Found x-pavatar in head: $faviconURL");
                }
                else if (!$islocalhost && ($mode=='P' && preg_match('/<link[^>]+rel="pavatar"[^>]+?href="([^"]+?)"/si', $fContent, $matches)) ||
                                          ($mode=='F' && preg_match('/<link[^>]+rel="(?:shortcut )?icon"[^>]+?href="([^"]+?)"/si', $fContent, $matches)))
                {
                    // Attempt to grab an avatar link from their webpage url
                    $linkUrl = (function_exists('serendipity_entity_decode') ? serendipity_entity_decode($matches[1]) : html_entity_decode($matches[1], ENT_COMPAT, LANG_CHARSET));
                    if (substr($linkUrl, 0, 1) == '/') {
                        if ($urlParts = parse_url($url)) {
                            $faviconURL = $urlParts['scheme'] . '://' . $urlParts['host'] . $linkUrl;
                        }
                    } else if (substr($linkUrl, 0, 7) == 'http://' || substr($linkUrl, 0, 8) == 'https://') {
                        $faviconURL = $linkUrl;
                    } else if (substr($url, -1, 1) == '/') {
                        $faviconURL = $url . $linkUrl;
                    } else {
                        $faviconURL = $url . '/' . $linkUrl;
                    }

                    $this->log($mode . " - Found link rel to url $faviconURL");
                } else {
                    // If unsuccessful, attempt to "guess" the favicon location
                    $urlParts   = parse_url($url);
                    $faviconURL = $urlParts['scheme'] . '://' . $urlParts['host'] . ($mode=='F'?'/favicon.ico':'/pavatar.png');
                    $this->log($mode . " - Not found link rel, guessing $faviconURL");
                }

                // Split image URL and check if image is available using a fast and timed out socket:
                $url_parts = @parse_url($faviconURL);
                if (!is_array($url_parts)) {
                    $url_parts = array();
                }

                if (!empty($url_parts['path'])) {
                    $documentpath = $url_parts['path'];
                } else {
                    $documentpath = '/';
                }

                if (!empty($url_parts['query'])) {
                    $documentpath .= '?' . $url_parts["query"];
                }

                if (empty($url_parts['port'])) {
                    $url_parts['port'] = '80';
                }

                if (!empty($url_parts['host'])) {
                    $socket = @fsockopen($url_parts['host'], $url_parts['port'], $errno, $errstr, 30);

                    if ($socket) {
                        fwrite($socket, "HEAD " . $documentpath . " HTTP/1.0\r\nHost: {$url_parts['host']}\r\n\r\n");
                        $http_response = fgets($socket, 25);

                        $this->log($mode . ' Testing server ' . $url_parts['host'] . " dopath: $documentpath - HEAD Response: $http_response");

                        if (preg_match($responses, $http_response)) // We only test for server existance
                        {
                            $favicon = $faviconURL;
                        }
                        fclose($socket);
                    }
                }

                // Remember the last result of the P/Favatar search
                $this->avatarConfiguration['img_url_'.$mode] = $favicon;
            } catch (HTTP_Request2_Exception $e) {
                if (!$islocalhost && preg_match($responses, $response->getStatus())) {
                    $favicon = false;
                    $this->log($mode . " - Error fetching $url: " . $response->getStatus());
                }
            }
            
            if (function_exists('serendipity_request_end')) {
                serendipity_request_end();
            }
        } // if favicon url not loaded from cache

        if (!empty($favicon)) {
            $this->log($mode . " - found at: $favicon");
            return $this->saveAndResponseAvatar($eventData, $favicon);
        }
        else {
            return false;
        }
    }

    /**
     * Shows a monster id avatar.
     *
     * @param array eventdata the data given by the event
     *
     * @return boolean true, if Avatar was found and added to the comment buffer
     */
    function fetchMonster(&$eventData)
    {
        require_once dirname(__FILE__) . '/monsterid/monsterid.php';

        $seed = $this->pluginHash($eventData['author']) . $eventData['email_md5'] . $this->pluginHash($eventData['url']);
        $default = $this->getDefaultImageConfiguration();
        $size = $default['size'];

        // Save monster image
        if (! file_exists($this->getCacheDirectory())) {
            @mkdir($this->getCacheDirectory());
        }
        $cache_file = $this->getCacheFilePath($eventData);
        $this->log("Caching monster avatar: " . $cache_file);
        
        if (build_monster($cache_file, $seed, $size)) {
            $this->log("Success caching monster.");
            $this->avatarConfiguration['mime-type'] = "image/png";
            $this->show($cache_file);
        }
        else if ($this->supportDefaultAvatar()){
            $this->fetchDefault();
        }

        return true;
    }

    /**
     * Shows a monster id avatar.
     *
     * @param array eventdata the data given by the event
     *
     * @return boolean true, if Avatar was found and added to the comment buffer
     */
    function fetchWavatar(&$eventData)
    {
        require_once dirname(__FILE__) . '/wavatars/wavatars.php';

        $seed = $this->pluginHash($eventData['author']) . $eventData['email_md5'] . $this->pluginHash($eventData['url']);
        $default = $this->getDefaultImageConfiguration();
        $size = $default['size'];

        // Save monster image
        if (! file_exists($this->getCacheDirectory())) {
            @mkdir($this->getCacheDirectory());
        }
        $cache_file = $this->getCacheFilePath($eventData);
        $this->log("Caching wavatar avatar: " . $cache_file);
        wavatar_build($seed, $cache_file, $size);
        if (file_exists($cache_file)) {
            $this->log("Success caching wavatar.");
            $this->avatarConfiguration['mime-type'] = "image/png";
            $this->show($cache_file);
        }
        else if ($this->supportDefaultAvatar()){
            $this->fetchDefault();
        }

        return true;
    }

    /**
     * Shows an identicon/ycon avatar (generated locally).
     * http://www.docuverse.com/blog/donpark/2007/01/18/visual-security-9-block-ip-identification
     * http://www.evilissexy.com/
     *
     * @param array eventdata the data given by the event
     *
     * @return boolean true, if Avatar was found and added to the comment buffer
     */
    function fetchYcon(&$eventData)
    {
        require_once dirname(__FILE__) . '/ycon/ycon.image.php';

        $seed = $this->pluginHash($eventData['author']) . $eventData['email_md5'] . $this->pluginHash($eventData['url']);
        $default = $this->getDefaultImageConfiguration();
        $size = $default['size'];

        // Save monster image
        if (! file_exists($this->getCacheDirectory())) {
            @mkdir($this->getCacheDirectory());
        }
        $cache_file = $this->getCacheFilePath($eventData);
        $this->log("Caching Identicon/Ycon avatar: " . $cache_file);
        if (build_ycon($cache_file,$seed,$size)) {
            $this->log("Success caching ycon.");
            $this->avatarConfiguration['mime-type'] = "image/png";
            $this->show($cache_file);
        }
        else if ($this->supportDefaultAvatar()){
            $this->fetchDefault();
        }

        return true;
    }

    /**
     * Shows the local default avatar.
     *
     * @param array eventdata the data given by the event
     *
     * @return boolean true, if Avatar was found and added to the comment buffer
     */
    function fetchDefault()
    {
        global $serendipity;

        $default = $this->getDefaultImageConfiguration();
        if (empty($default['defaultavatar'])) {
            return false;
        }

        $this->log("FetchDefault");
        // Set fetch date. Show will use this for caclculating cache.
        $this->avatarConfiguration['fetch_date'] = time();
        // Show default avatar
        $defaultUrl = $default['defaultavatar'];
        $this->log("DefaultUrl CFG: " . $defaultUrl);
        $defaultUrl = preg_replace('@^http[s]*?://[^/]*?/@si','',$defaultUrl); // Remove protocol and server part, if entered.
        $this->log("DefaultUrl RPL: " . $defaultUrl);
        $this->log("FetchDefault: DOC_ROOT" . $_SERVER["DOCUMENT_ROOT"]);
        $this->show($_SERVER["DOCUMENT_ROOT"] . '/' . $defaultUrl);

        return true;
    }

    /**
     * Caches an avatar and streams it back to the browser.
     */
    function saveAndResponseAvatar($eventData, $url, $allow_redirection = 3)
    {
        require_once S9Y_PEAR_PATH . 'HTTP/Request2.php';
        global $serendipity;
        $fContent   = null;

        if (function_exists('serendipity_request_start')) {
            serendipity_request_start();
        }

        if ($allow_redirection) {
            $request_pars['follow_redirects'] = true;
            $request_pars['max_redirects'] = $allow_redirection;
        }
        else {
            $request_pars['follow_redirects'] = false;
        }

        if (version_compare(PHP_VERSION, '5.6.0', '<')) {
            // On earlier PHP versions, the certificate validation fails. We deactivate it on them to restore the functionality we had with HTTP/Request1
            $request_pars['ssl_verify_peer'] = false;
        }

        $req = new HTTP_Request2($url, HTTP_Request2::METHOD_GET, $request_pars);
        try {
            $response = $req->send();
            if ($response->getStatus() != '200') {
                throw new HTTP_Request2_Exception("Could not fetch avatar");
            }
            // Allow only images as Avatar!
            $mime = $response->getHeader("content-type");
            $this->avatarConfiguration['mime-type'] = $mime;
            $this->log("Avatar fetch mimetype: $mime"  . " for url=" . $url);
            $mimeparts = explode('/',$mime);
            if (count($mimeparts)==2 && $mimeparts[0]=='image') {
                $fContent = $response->getBody();
            }
        } catch (HTTP_Request2_Exception $e) {
            $fContent = null;
            $this->log("Avatar fetch error: " . $e);
            
        }        

        if (function_exists('serendipity_request_start')) {
            serendipity_request_end();
        }

        // if no content was fetched, return false
        if (!isset($fContent) || empty($fContent)){
            $this->log("Avatar fetch: no Content!");
            return false;
        }

        $cache_file = $this->cacheAvatar($eventData, $fContent);
        if ($cache_file) {
            $this->show($cache_file);
        }
        else if ($this->supportDefaultAvatar()){
            $this->fetchDefault();
        }

        return true;
    }

    /**
     * Caches an avatar file.
     *
     * @param string cache_file name of file used for caching
     * @param string fContent content to be cached
     */
    function cacheAvatar($eventData, $fContent)
    {

        $cache_file = $this->getCacheFilePath($eventData);
        $this->log("cacheAvatar: " . $cache_file);

        // Save image
        if (! file_exists($this->getCacheDirectory())) {
            mkdir($this->getCacheDirectory());
        }
        $fp = @fopen($cache_file, 'wb');
        if (!$fp) {
            $this->log("! Error writing cache file $cache_file");
            if (file_exists($cache_file)) {
                return $cache_file;
            }
            else {
                return false;
            }
        }
        fwrite($fp, $fContent);
        fclose($fp);

        return $cache_file;
    }

    /**
     * Return binary response for an image
     */
    function show($filename)
    {
        $this->log("show: $filename");
        if (!file_exists($filename)) {
            header('X-Avatar: No-Image');
            return false;
        } else {
            header('X-Avatar: Found');
        }
        $mime_type = null;
        if (isset($this->avatarConfiguration['mime-type'])) {
            $mime_type = $this->avatarConfiguration['mime-type'];
        }
        if (!isset($mime_type)) {
            $size = @getimagesize($filename);
            $mime_type = $size['mime'];
            $this->avatarConfiguration['mime-type'] = $mime_type;
        }

        // test wether this really is (at least declared as) an image!
        // else deny it.
        $mime_parts  = explode('/', $mime_type);
        if (count($mime_parts)!=2 || $mime_parts[0]!='image') {
            return false;
        }

        $fp   = @fopen($filename, "rb");
        if ($fp) {
            if (isset($this->avatarConfiguration['fetch_date'])) {
                $filemtime = $this->avatarConfiguration['fetch_date'];
                $this->log("Fetch date found: " . date("D, d M Y H:i:s T", $filemtime));
            } else {
                $filemtime = filemtime($filename);
            }
            header("Content-type: $mime_type");
            header("Content-Length: ". filesize($filename));
            header("Date: " . date("D, d M Y H:i:s T"));
            header("Last-Modified: " . date("D, d M Y H:i:s T", $filemtime), true);
            if ($this->cache_seconds>0) {
                $expires = $filemtime + $this->cache_seconds;
                $expires_txt = date("D, d M Y H:i:s T",$expires);
                header("Expires: $expires_txt". true);
                // HTTP 1.1
                $max_age_seconds = $filemtime + $this->cache_seconds - time();
                header("Cache-Control: public, max-age=" . $max_age_seconds, true); // delta seconds
            }
            fpassthru($fp);
            fclose($fp);
        }
        return true;
    }

    /**
     * Generates an Avatar image from given parameters
     *
     * @param array default The default configuration evaluated
     * @param string title the title for that image
     * @return string the html code representing the Avatar
     */
    function generateImageHtml($url, $title = null, $align = 'right', $addAlignClass = true, $cssClass = "comment_avatar")
    {
        $default = $this->getDefaultImageConfiguration();

        if (empty($title)){
            $title = 'Avatar';
        }
        if (PLUGIN_EVENT_GRAVATAR_DEBUG) $title .= ' (Avatar Plugin V.' . PLUGIN_EVENT_GRAVATAR_VERSION . ' DEBUG)';

        // set alignment by configuration
        $cssAlign = '';
        if ($addAlignClass && ($align == 'right' || $align == 'left'))
            $cssAlign = "avatar_$align";
        $alt = '*';
        if (serendipity_db_bool($this->get_config('autoralt', 'false'))) {
            $alt = $title;
        }
        return '<img src="' . $url . '" alt="' . $alt . '" title="' . $title . '" class="' . $cssClass . ($addAlignClass? " $cssAlign":"") . '" height="' . $default['size'] . '" width="' . $default['size'] . '"/>';
    }

    /**
     * Just generates comments into the comment block. Used for debugging only!
     */
    function generateComment(&$eventData, $comment)
    {
        $eventData['comment'] = "-- $comment --<br>\n" . $eventData['comment'];
    }

    /**
     * Returns the avatar cache directory
     */
    function getCacheDirectory()
    {
        global $serendipity;
        if ($this->cache_dir === null) {
            $this->cache_dir = $serendipity['serendipityPath'] . PATH_SMARTY_COMPILE . '/serendipity_event_avatar';
        }
        return $this->cache_dir;
    }

    /**
     * Returns the Path of the avatar cache file by the given user data found in eventData
     * If no relevant user data was found, null is returned.
     *
     */
    function getCacheFilePath($eventData)
    {
        global $serendipity;

        $cache_filename = $this->getCacheFileName($eventData);
        if (!isset($cache_filename)) {
            return null;
        }
        return $this->getCacheDirectory() .'/' .  $cache_filename;;
    }

    /**
     * Returns the URL of the cached avatar by the given user data found in eventData
     * If no relevant user data was found, null is returned.
     *
     */
    function getCacheFileUrl($eventData)
    {
        global $serendipity;

        $cache_filename = $this->getCacheFileName($eventData);
        if (!isset($cache_filename)) {
            return null;
        }
        return $serendipity['baseURL'] . '/' . PATH_SMARTY_COMPILE . '/serendipity_event_gravatar/' . $cache_filename;
    }

    /**
     * Returns a URL encoded and signed variable.
     */
    function urlencode($url)
    {
        $hash = $this->pluginHash($this->instance . $url);
        return $hash . str_replace ('_', '%5F', urlencode($url));
    }

    function urldecode($url)
    {
        $hash     = substr($url, 0, 64);
        $real_url = urldecode(substr($url, 64));

        if ($hash == $this->pluginHash($this->instance . $real_url)) {
            // Valid hash was found.
            return $real_url;
        } else {
            // Invalid hash.
            return '';
        }
    }

    /**
     * Returns only the name of the cached avatar by the given user data found in eventData
     * If no relevant user data was found, null is returned.
     *
     */
    function getCacheFileName($eventData)
    {
        global $serendipity;

        if (!isset($eventData['email']) && !isset($eventData['email_md5']) && !isset($eventData['url'])) {
            return null;
        }
        $email_md5 = '';
        if (isset($eventData['email_md5'])) {
            $email_md5 = $eventData['email_md5'];
        }
        else if (isset($eventData['email'])) {
            $email_md5 = $this->pluginHash(strtolower($eventData['email']));
        }

        $author_md5= isset($eventData['author'])? $this->pluginHash($eventData['author']) : '';
        $url_md5 = isset($eventData['url'])? $this->pluginHash($eventData['url']) : '' ;

        return $url_md5 . '_' . $email_md5 . '_' . $author_md5;
    }

    /**
     * Builds an array of default image configuration
     */
    function getDefaultImageConfiguration()
    {
        global $serendipity;

        if (($this->defaultImageConfigurationdefault ?? null) === null) {
            $this->defaultImageConfigurationdefault = array(
                'defaultavatar' => ($this->get_config('defaultavatar')==''?'': $this->get_config('defaultavatar', '')),
                'size'          => $this->get_config('size', '40'),
                'rating'        => $this->get_config('rating', '-')
            );
        }
        return $this->defaultImageConfigurationdefault;
    }

    function getPermaPluginPath()
    {
        global $serendipity;

        // Get configured plugin path:
        $pluginPath = 'plugin';
        if (isset($serendipity['permalinkPluginPath'])){
            $pluginPath = $serendipity['permalinkPluginPath'];
        }

        return $pluginPath;

    }

    function log($message)
    {
        if (!PLUGIN_EVENT_GRAVATAR_DEBUG) return;
        $fp = fopen($this->getCacheDirectory() . '.log','a');
        fwrite($fp, $message . "\n");
        fclose($fp);
    }

}

/* vim: set sts=4 ts=4 expandtab : */
?>