<?php # $Id: serendipity_event_karma.php 2498 2009-04-16 08:02:53Z garvinhicking $


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

// Probe for a language include with constants. Still include defines later on, if some constants were missing
$probelang = dirname(__FILE__) . '/' . $serendipity['charset'] . 'lang_' . $serendipity['lang'] . '.inc.php';
if (file_exists($probelang)) {
    include $probelang;
}

include dirname(__FILE__) . '/lang_en.inc.php';

@define('PLUGIN_KARMA_DB_VERSION', '2.0');

class serendipity_event_karma extends serendipity_event
{
    /** @var string Status of current voting action */
    var $karmaVote    = '';
    /** @var int ID of entry to vote on */
    var $karmaId      = '';
    /** @var int Minutes until voting will be enabled, if a timeout is in effect */
    var $karmaTimeOut = '';
    /** @var int Points for this entry: current vote until voting is recorded,
     *  total points after voting is recorded 
     */
    var $karmaVoting  = '';

    /** @var string The name of the image to be used in graphic rating bars */
    var $image_name = 'stars-def-yellow-green-md.png';
    /** @var int The width of the rating bar image */
    var $image_width = '';
    /** @var int The height of the rating bar image */
    var $image_height = '';

    /** @var string The HTML to be used in backend preview */
    var $select_html = '';
    /** @var string The CSS to be added for backend preview */
    var $select_css = '';

    /** @var string Title if included as sidebar content */
    var $title        = PLUGIN_KARMA_NAME;

    /* The max width/height ratio of a single-segment image */
    var $max_segment_ratio = 1.0;

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

        $propbag->add('name',          PLUGIN_KARMA_NAME);
        $propbag->add('description',   PLUGIN_KARMA_BLAHBLAH);
        $propbag->add('stackable',     false);
        $propbag->add('author',        'Garvin Hicking, Grischa Brockhaus, Gregor Völtz, Judebert');
        $propbag->add('version',       '2.3');
        $propbag->add('requirements',  array(
            'serendipity' => '0.8',
            'smarty'      => '2.6.7',
            'php'         => '4.1.0'
        ));
        $propbag->add('event_hooks',   array(
            'frontend_configure'          => true, 
            'entry_display'               => true, 
            'css'                         => true, 
            'backend_header'              => true, 
            'backend_sidebar_entries'     => true,
            'backend_sidebar_entries_event_display_karmalog' => true,
            'event_additional_statistics' => true
            ));
        $propbag->add('groups', array('STATISTICS'));
        $propbag->add('configuration', array(
            // Functionality options
            'options_tab',
            'karma_active', 
            'extended_only', 
            'max_karmatime', 
            'max_votetime', 
            'max_entrytime', 
            'min_disp_votes',
            'visits_active', 
            'track_visits_of_loggedin_users', 
            'min_disp_visits',
            'exits_active', 
            'logging', 
            // Appearance options
            'appearance_tab',
            //--JAM:'alignment',
            'rate_with_words',
            'textual_msg',
            'textual_current',
            'textual_visits',
            'preview_bg',
            'base_image',
            // Text/Language options
            'text_tab',
            'rate_msg',
            'curr_msg',
            'rate_best',
            'rate_good',
            'rate_okay',
            'rate_poor',
            'rate_vile',
            'text_best',
            'text_good',
            'text_okay',
            'text_poor',
            'text_vile',
            // Close divs
            'end_tabs',
            ));
    }

    function introspect_config_item($name, &$propbag)
    {
        switch($name) {
            // Top of tab bar
            case 'options_tab':
                $propbag->add('type', 'content');
                $propbag->add('default', '
<span class="serendipity_karmaVoting_tabbar">
<a class="serendipity_karmaVoting_optionstab_link" href="#karmaVoting_options">' . PLUGIN_KARMA_TAB_OPTIONS . '</a> | 
<a class="serendipity_karmaVoting_appearancetab_link" href="#karmaVoting_appearance">' . PLUGIN_KARMA_TAB_APPEARANCE . '</a> |
<a class="serendipity_karmaVoting_texttab_link" href="#karmaVoting_text">' . PLUGIN_KARMA_TAB_TEXT . '</a>
</span>
<div class="serendipity_karmaVoting_optionstab" style="text-align: center;"><a name="karmaVoting_options"></a>
<span style="font-size: 10pt; font-weight: bold;">' . PLUGIN_KARMA_TAB_OPTIONS . '<hr style="width: 80%" />
');
                break;
            case 'appearance_tab':
                $propbag->add('type', 'content');
                $propbag->add('default', '
    <input class="serendipityPrettyButton input_button" type="submit" value="' . SAVE . '" name="SAVECONF" />
</div>
<div class="serendipity_karmaVoting_appearancetab" style="text-align: center;"><a name="karmaVoting_appearance"></a>
<span style="font-size: 10pt; font-weight: bold;">' . PLUGIN_KARMA_TAB_APPEARANCE . '<hr style="width: 80%" />
');
                break;
            case 'text_tab':
                $propbag->add('type', 'content');
                $propbag->add('default', '
    <input class="serendipityPrettyButton input_button" type="submit" value="' . SAVE . '" name="SAVECONF" />
</div>
<div class="serendipity_karmaVoting_texttab" style="text-align: center;"><a name="karmaVoting_text"></a> 
<span style="font-size: 10pt; font-weight: bold;">' . PLUGIN_KARMA_TAB_TEXT . '<hr style="width: 80%" />
');
                break;
            case 'end_tabs':
                $propbag->add('type', 'content');
                $propbag->add('default', '
</div>
');
                break;
            // Oldest entry age still valid for unrestricted voting
            case 'max_entrytime':
                $propbag->add('type', 'string');
                $propbag->add('name', PLUGIN_KARMA_ENTRYTIME);
                $propbag->add('description', PLUGIN_KARMA_ENTRYTIME_BLAHBLAH);
                $propbag->add('default', 1440);
                break;

            // Min time between votes
            case 'max_votetime':
                $propbag->add('type', 'string');
                $propbag->add('name', PLUGIN_KARMA_VOTINGTIME);
                $propbag->add('description', PLUGIN_KARMA_VOTINGTIME_BLAHBLAH);
                $propbag->add('default', 5);
                break;

            // Oldest entry age still valid for karma voting (normal, restricted voting)
            case 'max_karmatime':
                $propbag->add('type', 'string');
                $propbag->add('name', PLUGIN_KARMA_MAXKARMA);
                $propbag->add('description', PLUGIN_KARMA_MAXKARMA_BLAHBLAH);
                $propbag->add('default', 7);
                break;

            // Is karma voting allowed?
            case 'karma_active':
                $propbag->add('type', 'boolean');
                $propbag->add('name', PLUGIN_KARMA_ACTIVE);
                $propbag->add('description', PLUGIN_KARMA_ACTIVE_BLAHBLAH);
                $propbag->add('default', 'true');
                break;

            // Min votes to display
            case 'min_disp_votes':
                $propbag->add('type', 'string');
                $propbag->add('name', PLUGIN_KARMA_MIN_DISPLAYABLE_VOTES);
                $propbag->add('description', PLUGIN_KARMA_MIN_DISPLAYABLE_VOTES_BLAHBLAH);
                $propbag->add('default', '0');
                break;
                
            // Is extended article visit tracking allowed?
            case 'visits_active':
                $propbag->add('type', 'boolean');
                $propbag->add('name', PLUGIN_KARMA_VISITS);
                $propbag->add('description', PLUGIN_KARMA_VISITS_BLAHBLAH);
                $propbag->add('default', 'true');
                break;

            // Keep or ignore visits for logged in users (like admins/authors)?
            case 'track_visits_of_loggedin_users':
                $propbag->add('type', 'boolean');
                $propbag->add('name', PLUGIN_KARMA_VISITS_LOGGEDIN_USERS);
                $propbag->add('description', PLUGIN_KARMA_VISITS_LOGGEDIN_USERS_BLAHBLAH);
                $propbag->add('default', 'true');
                break;

            // Min visits to display
            case 'min_disp_visits':
                $propbag->add('type', 'string');
                $propbag->add('name', PLUGIN_KARMA_MIN_DISPLAYABLE_VISITS);
                $propbag->add('description', PLUGIN_KARMA_MIN_DISPLAYABLE_VISITS_BLAHBLAH);
                $propbag->add('default', '0');
                break;

            // Are we tracking exits?  
            case 'exits_active':
                $propbag->add('type', 'boolean');
                $propbag->add('name', SHOWS_TOP_EXIT);
                $propbag->add('description', '');
                $propbag->add('default', 'false');
                break;

            // Show karma voting on article summary, or only extended page?
            case 'extended_only':
                $propbag->add('type', 'boolean');
                $propbag->add('name', PLUGIN_KARMA_EXTENDEDONLY);
                $propbag->add('description', PLUGIN_KARMA_EXTENDEDONLY_BLAHBLAH);
                $propbag->add('default', 'false');
                break;

            // Log karma votes?
            case 'logging':
                $propbag->add('type', 'boolean');
                $propbag->add('name', PLUGIN_KARMA_LOGGING);
                $propbag->add('description', PLUGIN_KARMA_LOGGING_BLAHBLAH);
                $propbag->add('default', 'false');
                break;

            /*--JAM:
            // Alignment to use for rating bars
            case 'alignment':
                $select = array(
                            //--JAM: 'detect' => PLUGIN_KARMA_ALIGNMENT_DETECT,
                            'left' => LEFT,
                            'center' => CENTER,
                            'right' => RIGHT);
                $propbag->add('type', 'select');
                $propbag->add('name', PLUGIN_KARMA_ALIGNMENT);
                $propbag->add('description', PLUGIN_KARMA_ALIGNMENT_BLAHBLAH);
                $propbag->add('select_values', $select);
                $propbag->add('default', 'right');
                break;
             */
            // Use words for ratings?
            case 'rate_with_words':
                $propbag->add('type', 'boolean');
                $propbag->add('name', PLUGIN_KARMA_WORDRATING);
                $propbag->add('description', PLUGIN_KARMA_WORDRATING_BLAHBLAH);
                $propbag->add('default', 'false');
                break;
            case 'textual_msg':
                $propbag->add('type', 'boolean');
                $propbag->add('name', PLUGIN_KARMA_IMAGE_WITH_MESSAGE);
                $propbag->add('description', PLUGIN_KARMA_IMAGE_WITH_MESSAGE_BLAHBLAH);
                $propbag->add('default', 'true');
                break;
            case 'textual_current':
                $propbag->add('type', 'boolean');
                $propbag->add('name', PLUGIN_KARMA_IMAGE_WITH_CURR);
                $propbag->add('description', PLUGIN_KARMA_IMAGE_WITH_CURR_BLAHBLAH);
                $propbag->add('default', 'true');
                break;
            case 'textual_visits':
                $propbag->add('type', 'boolean');
                $propbag->add('name', PLUGIN_KARMA_IMAGE_WITH_VISITS);
                $propbag->add('description', PLUGIN_KARMA_IMAGE_WITH_VISITS_BLAHBLAH);
                $propbag->add('default', 'true');
                break;
            // Background of admin preview table
            case 'preview_bg':
                $propbag->add('type', 'string');
                $propbag->add('name', PLUGIN_KARMA_PREVIEW_BG);
                $propbag->add('description', PLUGIN_KARMA_PREVIEW_BG_BLAHBLAH);
                $propbag->add('default', '');
                break;
            // Image to use for graphical rating bar
            case 'base_image':
                $propbag->add('type', 'content');
                $propbag->add('default', $this->createRatingSelector());
                break;
            case 'rate_msg':
                $propbag->add('type', 'string');
                $propbag->add('name', PLUGIN_KARMA_VOTETEXT_NAME);
                $propbag->add('description', PLUGIN_KARMA_VOTETEXT_NAME_BLAHBLAH);
                $propbag->add('default', PLUGIN_KARMA_VOTETEXT);
                break;
            case 'curr_msg':
                $propbag->add('type', 'string');
                $propbag->add('name', PLUGIN_KARMA_CURRENT_NAME);
                $propbag->add('description', PLUGIN_KARMA_CURRENT_NAME_BLAHBLAH);
                $propbag->add('default', PLUGIN_KARMA_CURRENT);
                break;
            case 'rate_best':
                $propbag->add('type', 'string');
                $propbag->add('name', PLUGIN_KARMA_VOTEPOINT_5_NAME);
                $propbag->add('description', PLUGIN_KARMA_VOTEPOINT_5_BLAHBLAH);
                $propbag->add('default', PLUGIN_KARMA_VOTEPOINT_5);
                break;
            case 'rate_good':
                $propbag->add('type', 'string');
                $propbag->add('name', PLUGIN_KARMA_VOTEPOINT_4_NAME);
                $propbag->add('description', PLUGIN_KARMA_VOTEPOINT_4_BLAHBLAH);
                $propbag->add('default', PLUGIN_KARMA_VOTEPOINT_4);
                break;
            case 'rate_okay':
                $propbag->add('type', 'string');
                $propbag->add('name', PLUGIN_KARMA_VOTEPOINT_3_NAME);
                $propbag->add('description', PLUGIN_KARMA_VOTEPOINT_3_BLAHBLAH);
                $propbag->add('default', PLUGIN_KARMA_VOTEPOINT_3);
                break;
            case 'rate_poor':
                $propbag->add('type', 'string');
                $propbag->add('name', PLUGIN_KARMA_VOTEPOINT_2_NAME);
                $propbag->add('description', PLUGIN_KARMA_VOTEPOINT_2_BLAHBLAH);
                $propbag->add('default', PLUGIN_KARMA_VOTEPOINT_2);
                break;
            case 'rate_vile':
                $propbag->add('type', 'string');
                $propbag->add('name', PLUGIN_KARMA_VOTEPOINT_1_NAME);
                $propbag->add('description', PLUGIN_KARMA_VOTEPOINT_1_BLAHBLAH);
                $propbag->add('default', PLUGIN_KARMA_VOTEPOINT_1);
                break;
            case 'text_best':
                $propbag->add('type', 'string');
                $propbag->add('name', PLUGIN_KARMA_VOTETEXT_5_NAME);
                $propbag->add('description', PLUGIN_KARMA_VOTETEXT_5_BLAHBLAH);
                $propbag->add('default', PLUGIN_KARMA_VOTETEXT_5);
                break;
            case 'text_good':
                $propbag->add('type', 'string');
                $propbag->add('name', PLUGIN_KARMA_VOTETEXT_4_NAME);
                $propbag->add('description', PLUGIN_KARMA_VOTETEXT_4_BLAHBLAH);
                $propbag->add('default', PLUGIN_KARMA_VOTETEXT_4);
                break;
            case 'text_okay':
                $propbag->add('type', 'string');
                $propbag->add('name', PLUGIN_KARMA_VOTETEXT_3_NAME);
                $propbag->add('description', PLUGIN_KARMA_VOTETEXT_3_BLAHBLAH);
                $propbag->add('default', PLUGIN_KARMA_VOTETEXT_3);
                break;
            case 'text_poor':
                $propbag->add('type', 'string');
                $propbag->add('name', PLUGIN_KARMA_VOTETEXT_2_NAME);
                $propbag->add('description', PLUGIN_KARMA_VOTETEXT_2_BLAHBLAH);
                $propbag->add('default', PLUGIN_KARMA_VOTETEXT_2);
                break;
            case 'text_vile':
                $propbag->add('type', 'string');
                $propbag->add('name', PLUGIN_KARMA_VOTETEXT_1_NAME);
                $propbag->add('description', PLUGIN_KARMA_VOTETEXT_1_BLAHBLAH);
                $propbag->add('default', PLUGIN_KARMA_VOTETEXT_1);
                break;

            default:
                    return false;
        }
        return true;
    }

    /**
     * Updates database schema when upgrading from older versions.
     */
    function checkScheme() {
        global $serendipity;

        $version = $this->get_config('dbversion', '0'); 

        if ($version == '1.1') {
            $q   = "ALTER TABLE {$serendipity['dbPrefix']}karma ADD visits INT(11) default 0";
            $sql = serendipity_db_schema_import($q);
            $this->set_config('dbversion', PLUGIN_KARMA_DB_VERSION);
        } elseif ($version == '1.0') {
            $q   = "ALTER TABLE {$serendipity['dbPrefix']}karma ADD visits INT(11) default 0";
            $sql = serendipity_db_schema_import($q);

            $q   = "CREATE TABLE {$serendipity['dbPrefix']}karmalog (
                        entryid int(11) default null,
                        points int(4) default null,
                        ip varchar(15),
                        user_agent varchar(255),
                        votetime int(11) default null
                    )";
            $sql = serendipity_db_schema_import($q);
            $this->set_config('dbversion', PLUGIN_KARMA_DB_VERSION);
        } elseif ($version != PLUGIN_KARMA_DB_VERSION) {
            $q   = "CREATE TABLE {$serendipity['dbPrefix']}karma (
                        entryid int(11) default null,
                        points int(4) default null,
                        votes int(4) default null,
                        lastvote int(10) {UNSIGNED} NULL,
                        visits int(11) default null
                    )";
            $sql = serendipity_db_schema_import($q);

            $q   = "CREATE TABLE {$serendipity['dbPrefix']}karmalog (
                        entryid int(11) default null,
                        points int(4) default null,
                        ip varchar(15),
                        user_agent varchar(255),
                        votetime int(11) default null
                    )";
            $sql = serendipity_db_schema_import($q);

            $q   = "CREATE INDEX kfetch ON {$serendipity['dbPrefix']}karma (entryid, lastvote);";
            $sql = serendipity_db_schema_import($q);

            $q   = "CREATE INDEX kentryid ON {$serendipity['dbPrefix']}karma (entryid);";
            $sql = serendipity_db_schema_import($q);
            $this->set_config('dbversion', PLUGIN_KARMA_DB_VERSION);
        }

        return true;
    }

    /**
     * Unused; would create sidebar content. 
     */
    function generate_content(&$title)
    {
        $title       = $this->title;
    }

    /**
     * Creates an array of entries and their exit counts.
     *
     * @param array entries The entries to get exit counts for
     * @param boolean get optional Whether to return the array (otherwise
     *   does all the calculations, but returns only true) default:false
     *
     * @return Boolean true when $get is false; otherwise the array of
     *   entries with their exit counts.
     */
    function prepareExits($entries, $get = false) {
        static $exits = null;
        global $serendipity;

        if ($exits === null) {
            $q = 'SELECT entry_id, SUM(count) AS exits
                    FROM ' . $serendipity['dbPrefix'] . 'exits
                   WHERE entry_id IN (' . implode(', ', $entries) . ') GROUP BY entry_id';

            $sql = serendipity_db_query($q);
            $exits = array();
            if (is_array($sql)) {
                foreach($sql AS $idx => $row) {
                    $exits[$row['entry_id']] = (int)$row['exits'];
                }
            }
        }

        if ($get) {
            return $exits[$entries];
        }

        return true;
    }

    function getExits($entryid, $get_prepared = false) {
        global $serendipity;
        static $karma_exits = null;

        if ($karma_exits === null) {
            $karma_exits = ' <span class="serendipity_karmaVoting_exits_sep">|</span> <span class="serendipity_karmaVoting_exits">' . TOP_EXITS . '</span> <span class="serendipity_karmaVoting_exits_num">(%d)</span>';
        }

        if ($get_prepared) {
            $points = $this->prepareExits($entryid, true);
        } else {
            $res = serendipity_db_query("SELECT sum(count) AS exits FROM {$serendipity['dbPrefix']}exits WHERE entry_id = " . (int)$entryid . " GROUP BY entry_id", true, 'assoc');
            if (is_array($res) && isset($res['exits'])) {
                $points = $res['exits'];
            } else {
                $points = 0;
            }
        }

        return sprintf($karma_exits, $points);
    }

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

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

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

            // Moved from above: only get image data if we're actually going to do something
            $this->set_valid_image_data();
            // Get dimensions of image, only if not text-only
            if ($this->image_name) {
                // Is this a single-image bar, or a single segment?
                $ratio = $this->image_width / $this->image_height;
                if ($ratio < $max_segment_ratio) {
                    // This is probably a single segment. Square segments
                    // will have a ratio of 0.3; long, flat segments won't
                    // get up to 1.0 unless they're 3 times as wide as they
                    // are tall; full-bar images with square segments will
                    // be 1.666; and full-bar images with tall, narrow
                    // segments will be greater than 1.0 unless they're 
                    // nearly twice as high as they are wide.
                    $this->image_width = $this->image_width * 5;
                }
            }

            switch($event) {
                // Early hook, before any page is displayed
                case 'frontend_configure':
                    
                    // Make sure the karmaVote cookie is set, even if empty <a name="#1" />
                    if (!isset($serendipity['COOKIE']['karmaVote'])) {
                        serendipity_setCookie('karmaVote', serialize(array()));
                    }

                    // If user didn't vote, we're done.
                    if (!isset($serendipity['GET']['karmaId']) || !isset($serendipity['GET']['karmaVote'])) {
                        return;
                    }

                    // Get URL vote data
                    $this->karmaId     = (int)$serendipity['GET']['karmaId'];
                    $this->karmaVoting = (int)$serendipity['GET']['karmaVote'];

                    // karmaVote cookie was just set (see name="#1"); this boils down to 
                    // "if check cookie isn't 1, there's no real cookie".
                    // The check cookie gets set when a rater is displayed,
                    // so you've got no business voting if you haven't even
                    // seen the rater yet.
                    if (!isset($serendipity['COOKIE']['karmaVote']) OR $serendipity['COOKIE']['check'] != '1') {
                        $this->karmaVote = 'nocookie';
                        return;
                    }

                    // Everything is ready.  Get the cookie vote data.
                    $karma   = unserialize($serendipity['COOKIE']['karmaVote']);

                    // Stop on invalid votes (cookie invalid, or URL data incorrect)
                    if (!is_array($karma) || !is_numeric($this->karmaVoting) || !is_numeric($this->karmaId) || $this->karmaVoting > 2 || $this->karmaVoting < -2) {
                        $this->karmaVote = 'invalid1';
                        return;
                    }

                    // Stop if the cookie says we already voted
                    if (!empty($karma[$this->karmaId])) {
                        $this->karmaVote = 'alreadyvoted';
                        return ;
                    }

                    // We don't want bots hitting the karma-voting
                    $agent = $_SERVER['HTTP_USER_AGENT'];
                    if (stristr($agent, 'google')
                        || stristr($agent, 'LinkWalker')
                        || stristr($agent, 'zermelo')
                        || stristr($agent, 'NimbleCrawler')) {
                        $this->karmaVote = 'invalid1';
                        return ;
                    }

                    // Voting takes place here.
                    // 
                    // Get voting data from the database (keeps all entries,
                    // even if no karma match)
                    $q = 'SELECT *
                            FROM ' . $serendipity['dbPrefix'] . 'entries AS e
                 LEFT OUTER JOIN ' . $serendipity['dbPrefix'] . 'karma   AS k
                              ON e.id = k.entryid
                           WHERE e.id = ' . serendipity_db_escape_string($this->karmaId) . ' LIMIT 1';
                    $row = serendipity_db_query($q, true);

                    // If there's no entry with this ID, we're done
                    //
                    // --TODO: Modify the plugin to allow arbitrary voting with generated IDs
                    if (!isset($row) || !is_array($row)) {
                        $this->karmaVote = 'invalid2';
                        return;
                    }

                    $now = time();
                    if ($row['votes'] === '0' || $row['votes'] > 0) {
                        // Votes for this entry already exist. Do some checking.
                        $max_entrytime = $this->get_config('max_entrytime', 1440) * 60;
                        $max_votetime  = $this->get_config('max_votetime', 5)     * 60;
                        $max_karmatime = $this->get_config('max_karmatime', 7)    * 24 * 60 * 60;
                        // Allow infinite voting when 0 or negative
                        if ($max_karmatime <= 0) {
                            $max_karmatime = $now;
                        }

                        // If the entry's timestamp is too old for voting,
                        // we're done.
                        if ($row['timestamp'] < ($now - $max_karmatime)) {
                            $this->karmaVote = 'timeout2';
                            return;
                        }

                        // If the entry is in the grace period, or votes
                        // aren't too close together, record the vote.
                        if (($row['timestamp'] > ($now - $max_entrytime)) || ($row['lastvote'] + $max_votetime < $now) || $row['lastvote'] == 0) {
                            // Update votes
                            $q = sprintf(
                              "UPDATE {$serendipity['dbPrefix']}karma
                                  SET points   = %s,
                                      votes    = %s,
                                      lastvote = %s
                                WHERE entryid  = %s",
                              $row['points'] + $this->karmaVoting,
                              $row['votes']  + 1,
                              $now,
                              $this->karmaId
                            );

                            serendipity_db_query($q);
                        } else {
                            // Entry was too recently voted upon.  Figure out 
                            // how long until voting will be allowed (in minutes).
                            $this->karmaVote    = 'timeout';
                            $this->karmaTimeOut = abs(ceil(($now - ($row['lastvote'] + $max_votetime)) / 60));
                            return;
                        }
                    } else {
                        // No row. Use INSERT instead of UPDATE.
                        $q = sprintf(
                          "INSERT INTO {$serendipity['dbPrefix']}karma
                                       (entryid, points, votes, lastvote, visits)
                                VALUES (%s,      %s,     %s,    %s,       %s)",
                          $this->karmaId,
                          $this->karmaVoting,
                          1,
                          $now,
                          0
                        );

                        $sql = serendipity_db_query($q);
                    }

                    // Log the vote
                    if (serendipity_db_bool($this->get_config('logging', false))) {
                        $q = sprintf(
                          "INSERT INTO {$serendipity['dbPrefix']}karmalog
                                       (entryid, points, ip, user_agent, votetime)
                                VALUES (%s, %s, '%s', '%s', %s)",
                          $this->karmaId,
                          $this->karmaVoting,
                          serendipity_db_escape_string($_SERVER['REMOTE_ADDR']),
                          substr(serendipity_db_escape_string($_SERVER['HTTP_USER_AGENT']), 0, 255),
                          $now
                        );
                        $sql = serendipity_db_query($q);
                        if (is_string($sql)) {
                            mail($serendipity['serendipityEmail'] , 'KARMA ERROR', $q . '<br />' . $sql . '<br />');
                        }
                    }

                    // Set the cookie that we already voted for this entry
                    $karma[$this->karmaId] = $this->karmaVoting;
                    $this->karmaVote       = 'voted';
                    serendipity_setCookie('karmaVote', serialize($karma));

                    return true;
                    break;

                // CSS generation hooks
                case 'backend_header':
                    // Generate the CSS for the graphical rating bar selector
                    // 
                    // The CSS appears to be generated in a completely 
                    // different instance of Serendipity, as if index.php gets
                    // called separately for the CSS.
                    //
                    // Note that the css_backend hook adds properties to the 
                    // serendipity_admin.css, but that file is *always* 
                    // cached.  We use backend_header and add the CSS to the
                    // HEAD styles to make it dynamic.
                    
                    // Get the CSS, set $this->image_name so we'll output the
                    // standard graphical CSS prologue if any images are found.
                    $this->createRatingSelector();

                    print ("<style type='text/css'>\n");

                    $align = 'center';
                    $bg = $this->get_config('preview_bg', false);
                    if (!empty($bg)) {
                        if ((strpos($bg, ';') !== false) || (strpos($bg, ',') !== false)) {
                            $bg = 'red';
                        }
                        print("
.serendipity_karmaVote_selectorTable {
    background: $bg;
}
");
                    }
                    print("
.serendipityAdminContent .serendipity_karmaVoting_links {
    margin: 5px;
}
");
                case 'css':
                    // Some CSS notes:
                    // 
                    // .serendipity_karmaVoting is the class for the karma wrapper/container,
                    //      including the text explanations, messages, and rating bar.
                    //      (currently a div)
                    // .serendipity_karmaVoting a specifies the links for the text-mode
                    //      rating bar
                    // .serendipity_karmaError is the class for any text indicating an
                    //      error condition
                    // .serendipity_karmaSuccess is the class for any text indicating
                    //      successful operation
                    // .serendipity_karmaVoting_links is the container for the graphical
                    //      rating bar (currently an ol)
                    // .serendipity_karmaVoting_links a indicates the various voting links in
                    //      the graphical rating bar
                    // .serendipity_karmaVoting_current-rating is the class for the current
                    //      rating in the graphical rating bar
                    //  a.serendipity_karmaVoting_link1, _link2, etc are the classes applied 
                    //      to the individual voting links

                    // Note that there are two possible template types: early
                    // templates that only handle the text rating bars, and 
                    // newer templates that understand the graphical raters.
                    // We check for both types and act appropriately.
                    /*--JAM: Let's just skip this whole hassle
                    if (!$align) {
                        $align = $this->get_config('alignment', 'detect');
                    }
                    if ($align == 'detect') {
                    */
                    $align = $this->get_config('alignment', 'center');
                        // Try to let the template take care of it
                        if ($this->image_name == '0') {
                            // Text-only rating bar is used
                            if (strpos($eventData, '.serendipity_karmaVoting')) {
                                // Template is handling all our CSS
                                return true;
                            }
                        }/* --JAM: else {
                            // Graphical rating bar is used
                            if (strpos($eventData, '.serendipity_karmaVoting_images')) {
                                // Template is handling all our CSS
                                return true;
                            }
                            // Check for old text-only templates
                            $pos = strpos($eventData, '.serendipity_karmaVoting');
                            while ($pos && ($align == 'detect')) {
                                // Find text-align: in the current block
                                $endpos = strpos($eventData, '}', $pos);
                                if (!$endpos) {
                                    // Broken CSS
                                    break;
                                }
                                $alignpos = strpos($eventData, 'text-align:', $pos);
                                // Can't check for comments, or I would.  Hope
                                // the first is the correct one.
                                if ($alignpos && $alignpos < $endpos) {
                                    $start = $alignpos + 11;
                                    $alignend = strpos($eventData, ';', $alignpos);
                                    if ($alignend)
                                    {
                                        // All valid.  Pull out the alignment.
                                        $len = $alignend - $start;
                                        $align = trim(substr($eventData, $start, $len));
                                    }
                                }
                                $pos = strpos($eventData, '.serendipity_karmaVoting', $endpos);
                            }
                            // I should have a valid alignment or 'detect' in $align now. 
                        }
                    }
                    // If we couldn't detect the alignment, guess 'right'
                    if ($align == 'detect') {
                        $align = 'right';
                    }
                    --JAM: END COMMENT BLOCK */

                    // Since errors might be printed at any time, always
                    // output the text-mode CSS
                    print <<<EOS
.serendipity_karmaVoting {
    text-align: $align;
    font-size: 7pt;
    margin: 0px;
}

.serendipity_karmaVoting a {
    font-size: 7pt;
    text-decoration: none;
}

.serendipity_karmaVoting a:hover {
    color: green;
}

.serendipity_karmaError {
    color: #FF8000;
}
.serendipity_karmaSuccess {
    color: green;
}
EOS;
                    // Only output the image CSS if it's needed
                    if ($this->image_name != '0') {
                        $img = $serendipity['baseURL'] . "plugins/serendipity_event_karma/img/" . $this->image_name; 
                        $h = $this->image_height / 3;
                        $w = $this->image_width;
                        switch ($align) {
                            case 'left':
                                $margin = '0px auto 0px 0px';
                                break;
                            case 'center':
                                $margin = '0px auto';
                                break;
                            case 'right':
                            default:
                                $margin = '0px 0px 0px auto';
                                break;

                        }
                        // The CSS here is lifted largely from 
                        // http://komodomedia.com/blog/index.php/2007/01/20/css-star-rating-redux/
                        //
                        // Note, however that margin has been changed for
                        // multiple cases and all unitless measurements have 
                        // been specified in pixels.  Additionally, measures 
                        // have been taken to align the text.
                        print <<<END_IMG_CSS

.serendipity_karmaVoting_links,
.serendipity_karmaVoting_links a:hover,
.serendipity_karmaVoting_current-rating {
    background: url($img) left;
    font-size: 0;
}
.serendipity_karmaVoting_links {
    position: relative;
    width: {$w}px;
    height: {$h}px;
    overflow: hidden;
    list-style: none;
    margin: $margin;
    padding: 0px;
    background-position: left top;     
    text-align: center;
}
.serendipity_karmaVoting_links li {
   display: inline; 
}
.serendipity_karmaVoting_links a ,
.serendipity_karmaVoting_current-rating {
    position:absolute;
    top: 0px;
    left: 0px;
    text-indent: -9000em;
    height: {$h}px;
    line-height: {$h}px;
    outline: none;
    overflow: hidden;
    border: none;
}
.serendipity_karmaVoting_links a:hover {
    background-position: left bottom;
}
.serendipity_karmaVoting_links a.serendipity_karmaVoting_link1 {
    width: 20%;
    z-index: 6;
}
.serendipity_karmaVoting_links a.serendipity_karmaVoting_link2 {
    width: 40%;
    z-index: 5;
}
.serendipity_karmaVoting_links a.serendipity_karmaVoting_link3 {
    width: 60%;
    z-index: 4;
}
.serendipity_karmaVoting_links a.serendipity_karmaVoting_link4 {
    width: 80%;
    z-index: 3;
}
.serendipity_karmaVoting_links a.serendipity_karmaVoting_link5 {
  width: 100%;
    z-index: 2;
}
.serendipity_karmaVoting_links .serendipity_karmaVoting_current-rating {
    z-index: 1;
    background-position: left center;
}

END_IMG_CSS;
                        // Add selector images CSS, if necessary
                        if (!empty($this->select_css)) {
                            print($this->select_css);
                        }
                    } // End if image bar defined

                    if ($event == 'backend_header') {
                        print("\n</style>\n");
                    }
                    return true;
                    break;

                //--TODO: Comment the functionality of this event hook.
                case 'event_additional_statistics':
                    $sql = array();
                    $sql['visits_top']    = array('visits', 'DESC');
                    $sql['visits_bottom'] = array('visits', 'ASC');
                    $sql['votes_top']     = array('votes', 'DESC');
                    $sql['votes_bottom']  = array('votes', 'ASC');
                    $sql['points_top']    = array('points', 'DESC');
                    $sql['points_bottom'] = array('points', 'ASC');

                    foreach($sql AS $key => $rows) {
                        $q = "SELECT e.id,
                                     e.title,
                                     e.timestamp,
                                     SUM(k.{$rows[0]}) AS no
                                FROM {$serendipity['dbPrefix']}karma
                                     AS k
                                JOIN {$serendipity['dbPrefix']}entries
                                     AS e
                                  ON k.entryid = e.id
                            WHERE k.{$rows[0]} IS NOT NULL AND k.{$rows[0]} != 0
                            GROUP BY e.id, e.title, e.timestamp ORDER BY no {$rows[1]} LIMIT {$addData['maxitems']}";
                        $sql_rows = serendipity_db_query($q);
?>
    <dt><strong><?php echo constant('PLUGIN_KARMA_STATISTICS_' . strtoupper($key)); ?></strong></dt>
    <dl>
<?php
                        if (is_array($sql_rows)) {
                            foreach($sql_rows AS $id => $row) {
    ?>
        <dt><strong><a href="<?php echo serendipity_archiveURL($row['id'], $row['title'], 'serendipityHTTPPath', true, array('timestamp' => $row['timestamp'])); ?>"><?php echo htmlspecialchars($row['title']); ?></a></strong></dt>
        <dd><?php echo $row['no']; ?> <?php echo constant('PLUGIN_KARMA_STATISTICS_' . strtoupper($rows[0]) . '_NO'); ?></dd>
    <?php
                            }
                        }
?>
    </dl>
<?php
                    }

                    return true;
                    break;

                // Add voting information to entries
                case 'entry_display':
                    // Update database if necessary 
                    if ($this->get_config('dbversion', 0) != PLUGIN_KARMA_DB_VERSION) {
                        $this->checkScheme();
                    }
                    
                    // Find the ID of this entry
                    if (isset($serendipity['GET']['id'])) {
                        $entryid = (int)serendipity_db_escape_string($serendipity['GET']['id']);
                    } elseif (preg_match(PAT_COMMENTSUB, $_SERVER['REQUEST_URI'], $matches)) {
                        $entryid = (int)$matches[1];
                    } else {
                        $entryid = false;
                    }

                    // If we're actually reading the entry, not voting or editing it...
                    if ($entryid && empty($serendipity['GET']['adminAction']) && !$serendipity['GET']['karmaVote']) {
                        // Update the number of visits
                        // Are we supposed to track visits?
                        $track_clicks  = serendipity_db_bool($this->get_config('visits_active', true)) && $this->track_clicks_allowed_by_user();
                        if ($track_clicks && $_SERVER['REQUEST_METHOD'] == 'GET') {
                            $sql = serendipity_db_query(
                                "UPDATE {$serendipity['dbPrefix']}karma 
                                    SET visits = visits + 1 
                                  WHERE entryid = $entryid", 
                                  true);
                            if (serendipity_db_affected_rows() < 1) {
                                serendipity_db_query(
                                        "INSERT INTO {$serendipity['dbPrefix']}karma (entryid, points, votes, lastvote, visits) 
                                              VALUES ('$entryid', 0, 0, 0, 1)"
                                        );
                            }
                        }
                    }
                    
                    // Set a cookie to look for later, verifying that cookies are enabled
                    serendipity_setCookie('check', '1');

                    switch($this->karmaVote) {
                        case 'nocookie':
                            // Users with no cookies won't be able to vote.
                            $msg = '<div class="serendipity_karmaVoting serendipity_karmaError"><a id="karma_vote' . $this->karmaId . '"></a>' . PLUGIN_KARMA_NOCOOKIE . '</div>';
                            // Continue until output

                        case 'timeout2':
                            if (!isset($msg)) {
                                $msg = '<div class="serendipity_karmaVoting serendipity_karmaError"><a id="karma_vote' . $this->karmaId . '"></a>' . PLUGIN_KARMA_CLOSED . '</div>';
                            }
                            // Continue until output

                        case 'timeout':
                            if (!isset($msg)) {
                                $msg = '<div class="serendipity_karmaVoting serendipity_karmaError"><a id="karma_vote' . $this->karmaId . '"></a>' . sprintf(PLUGIN_KARMA_TIMEOUT, $this->karmaTimeOut) . '</div>';
                            }
                            // Continue until output

                        case 'alreadyvoted':
                            if (!isset($msg)) {
                                $msg = '<div class="serendipity_karmaVoting serendipity_karmaError"><a id="karma_vote' . $this->karmaId . '"></a>' . PLUGIN_KARMA_ALREADYVOTED . '</div>';
                            }
                            // Continue until output

                        case 'invalid1':
                        case 'invalid2':
                        case 'invalid':
                            // Set message
                            if (!isset($msg)) {
                                $msg = '<div class="serendipity_karmaVoting serendipity_karmaError"><a id="karma_vote' . $this->karmaId . '"></a>' . PLUGIN_KARMA_INVALID . '</div>';
                            }
                            // Continue until output

                            /* OUTPUT MESSAGE */
                            //--TODO: Shouldn't this work with the cache plugin, too?
                            if ($addData['extended']) {
                                $eventData[0]['exflag'] = 1;
                                $eventData[0]['add_footer'] .= $msg;
                            } else {
                                $elements = count($eventData);
                                // Find the right container to store our message in.
                                for ($i = 0; $i < $elements; $i++) {
                                    if ($eventData[$i]['id'] == $this->karmaId) {
                                        $eventData[$i]['add_footer'] .= $msg;
                                    }
                                }
                            }
                            break;

                        case 'voted':
                        default:
                            // If there's no data, there's no need to go on
                            if (!is_array($eventData)) return;

                            // Find out what the admin wants
                            $track_clicks  = serendipity_db_bool($this->get_config('visits_active', true));
                            $track_karma   = serendipity_db_bool($this->get_config('karma_active', true));
                            $track_exits   = serendipity_db_bool($this->get_config('exits_active', true));

                            // Get the limits
                            $now           = time();
                            $karmatime     = $this->get_config('max_karmatime', 7);
                            $max_karmatime = $karmatime    * 24 * 60 * 60;
                            // Accept infinite voting
                            if ($max_karmatime <= 0) {
                                $max_karmatime = $now;
                            }

                            //--TODO: Ensure that this works with the Custom Permalinks plugin
                            // (We've seen trouble; it votes correctly, but redirects to the front page)
                            $url = serendipity_currentURL(true);
                            // Voting is only allowed on entries.  Therefore voting URLs must be
                            // either single-entry URLs or summary URLs.  serendipity_currentURL
                            // converts them to an "ErrorDocument-URI", so we can focus on the
                            // query portion of the URI.
                            //
                            // Single-entry URLs should be well-defined.  They can be permalinks,
                            // of course; otherwise they're of the configured pattern.
                            //
                            // Summary URLs could be a little harder.  The summary pages that 
                            // include entries are: frontpage, category, author, and archives. 
                            // It's possible a plugin would show entries, but if that's the case
                            // we don't need to allow the user to vote on them.  Still, that's
                            // a lot of URLs to check for.
                            //
                            // Then there's the problem of the rest of the query.  It could 
                            // include stuff we really want to keep around, like template 
                            // overrides or something.  One can even add serendipity variables
                            // to the URL in extreme cases.
                            //
                            // It seems that canonicalizing the URL will be quite difficult.
                            // The only thing we can say for certain is that whatever the 
                            // current URL is, it got us to this page, and we'd like to return
                            // to this page after we cast our vote.
                            
                            // Remove any clutter from our previous voting activity
                            $url_parts = parse_url(serendipity_currentURL(true));
                            if (!empty($url_parts['query'])) {
                                $exclude = array('serendipity[karmaVote]', 'serendipity[karmaId]'); 
                                // I tried using parse_str, but it gave me very weird results
                                // with embedded arrays
                                //parse_str($url_parts['query'], $q_parts);
                                $q_parts = array();
                                // I don't know why this URL has been HTML encoded.  Oh well.
                                $pairs = explode('&amp;', $url_parts['query']);
                                foreach($pairs as $pair) {
                                    $parts = explode('=', $pair);
                                    $q_parts[$parts[0]] = $parts[1];
                                }
                                foreach($q_parts as $key => $value) {
                                    if (in_array($key, $exclude)) {
                                        $rm = preg_quote("$key=$value");
                                        $url = preg_replace("@(&amp;|&)?$rm@", '', $url);
                                    }
                                }
                            }
                            if (substr($url, -1) != '?') {
                                $url .= '&amp;';
                            }

                            // Get the cookie data (past votes, etc)
                            $karma = (isset($serendipity['COOKIE']['karmaVote']) ? unserialize($serendipity['COOKIE']['karmaVote']) : array());
                            // Get all required entry IDs, making keys match keys in eventData
                            $entries = array();
                            if ($addData['extended'] || $addData['preview']) {
                                // We're in extended or preview mode, we only need the current ID
                                $eventData[0]['exflag'] = 1;
                                $entries[0] = (int)($eventData[0]['id']);
                            } elseif (!serendipity_db_bool($this->get_config('extended_only', false))) {
                                // We're in overview mode, and we want rating bars for all the entry IDs
                                foreach(array_keys($eventData) as $key) {
                                    if (isset($eventData[$key]['id'])) {
                                        $entries[$key] = (int)$eventData[$key]['id'];
                                    }
                                }
                            }

                            // Fetch votes for all entry IDs. Store them in an array for later usage.
                            $q = 'SELECT k.entryid, SUM(votes) AS votes, SUM(points) AS points, SUM(visits) AS visits
                                    FROM ' . $serendipity['dbPrefix'] . 'karma   AS k
                                   WHERE k.entryid IN (' . implode(', ', $entries) . ') GROUP BY k.entryid';

                            $sql = serendipity_db_query($q);

                            $rows = array();
                            if ($sql && is_array($sql)) {
                                foreach($sql AS $row) {
                                    $rows[$row['entryid']] = array(
                                            'votes' => $row['votes'], 
                                            'points' => $row['points'], 
                                            'visits' => $row['visits']
                                            );
                                }
                            }

                            $this->prepareExits($entries);

                            // Add karma block to the footer of each entry
                            // 
                            // The entries array was populated, above, so its keys match the eventData array,
                            // and overview entries are skipped if "extended only" is enabled
                            foreach (array_keys($entries) as $i) {
                                // Get the statistics
                                $entryid = $eventData[$i]['id'];
                                $votes   = (!empty($rows[$entryid]['votes']) ? $rows[$entryid]['votes'] : 0);
                                $points  = (!empty($rows[$entryid]['points']) ? $rows[$entryid]['points'] : 0);
                                $visits  = (!empty($rows[$entryid]['visits']) ? $rows[$entryid]['visits'] : 0);
                                $enough_votes = $track_karma && ($votes >= $this->get_config('min_disp_votes', 0));
                                $enough_visits = $track_clicks && ($visits >= $this->get_config('min_disp_visits', 0));
                                $textual_msg = true;
                                $textual_current = true;
                                $textual_visits = true;
                                if ($this->image_name != '0') {
                                    $textual_msg = $this->get_config('textual_msg', 'true');
                                    $textual_current = $this->get_config('textual_current', 'true');
                                    $textual_visits = $this->get_config('textual_visits', 'true');
                                }

                                // Where's the footer?  Normally it would be
                                // in eventData[n]['add_footer'] but if the
                                // cache plugin is used, it's in 
                                // eventData[n]['properties']['ep_cache_add_footer'].
                                // This method retrieves it either way.
                                $footer = &$this->getFieldReference('add_footer', $eventData[$i]);

                                // Depending on what existed, $footer could
                                // be referencing the cached version, the
                                // uncached version, or even a new empty 
                                // string.  In particular, if $eventData[$i] 
                                // has no properties, and no 'add_footer' key, 
                                // $footer is referencing a new empty string,
                                // so adding a karma bar to $footer would do 
                                // nothing.
                                //
                                // We could be referencing an empty uncached
                                // 'add_footer', but empty cache entries are
                                // never returned.
                                //
                                // Reference a footer that will be printed
                                if (empty($footer) && !isset($eventData[$i]['add_footer']) && is_array($eventData[$i])) {
                                  $eventData[$i]['add_footer'] = '';
                                  $footer = &$eventData[$i]['add_footer'];
                                  // It's still empty, but it's referencing
                                  // the right place.
                                }

                                if ($track_exits) {
                                    $footer .= $this->getExits($entryid, true);
                                }

                                // Pick the appropriate intro msg and rating bar
                                // No msg or bar if karma is disabled
                                if ($track_karma) {
                                    if (isset($karma[$entryid])) {
                                        // We already voted for this one
                                        $msg =
    '<div class="serendipity_karmaSuccess">' . PLUGIN_KARMA_VOTED . '</div>';
                                        $myvote = $karma[$entryid];
                                        if ($this->get_config('rate_with_words', false)) {
                                            $myvote = $this->wordRating($myvote, 1);
                                        }
                                        elseif ($this->image_name != '0') {
                                            $myvote = $this->imageRating($myvote, 1);
                                        }
                                        // Just a current rating bar, if any
                                        $bar = $this->createRatingBar(null, $points, $votes);
                                    } elseif ($eventData[$i]['timestamp'] < ($now - $max_karmatime)) {
                                        // Too late to vote for this one
                                        $msg = 
    '<div class="serendipity_karmaClosed">' . sprintf(PLUGIN_KARMA_CLOSED, $karmatime) . '</div>';
                                        // Just a current rating bar, if any
                                        $bar = $this->createRatingBar(null, $points, $votes);
                                    } else {
                                        // We can vote for this; make the whole voting block
                                        $rate_msg = $this->get_config('rate_msg', PLUGIN_KARMA_VOTETEXT);
                                        $msg = 
    '<div class="serendipity_karmaVoting_text">' . $rate_msg . '</div>';
                                        // Full voting bar
                                        $bar = $this->createRatingBar($entryid, $points, $votes);
                                    }
                                }
                                // Create the karma block
                                $image_class = '';
                                if ($this->image_name != '0') {
                                    $image_class = ' serendipity_karmaVoting_images';
                                }
                                $karma_block = 
"<div class='serendipity_karmaVoting$image_class'><a id='karma_vote$entryid'></a>";
                                if ($textual_msg) {
                                    $karma_block .= 
    $msg;
                                }
                                $karma_block .=
    $bar;
                                if ($enough_votes && $textual_current) {
                                    $curr_msg = $this->get_config('curr_msg', PLUGIN_KARMA_CURRENT);
                                    $karma_block .=
    '<span class="serendipity_karmaVoting_current">' . $curr_msg . '</span>';
                                }
                                if ($enough_visits && $textual_visits) {
                                    $karma_block .= 
    '<span class="serendipity_karmaVoting_visits">' . PLUGIN_KARMA_VISITSCOUNT . '</span>';
                                }
                                $karma_block .=
"\n</div>\n";
                                // Adjust rating points
                                if ($this->get_config('rate_with_words', false)) {
                                    $points = $this->wordRating($points, $votes);
                                }
                                elseif ($this->image_name != '0') {
                                    $points = $this->imageRating($points, $votes);
                                }

                                /*
                                   print("<h3>--DEBUG: Karma block code:</h3>\n<pre>\n");
                                   print_r(htmlspecialchars($karma_block));
                                   print("\n</pre>\n");
                                 */

                                // Substitute the % stuff and add it to the footer
                                $eventData[$i]['properties']['myvote'] = $myvote;
                                $eventData[$i]['properties']['points'] = $points;
                                $eventData[$i]['properties']['votes'] = $votes;
                                $eventData[$i]['properties']['visits'] = $visits;

                                $footer .= sprintf($karma_block, $myvote, $points, $votes, $visits, $url);
                            } // foreach key in entries
                    }// End switch on karma voting status
                    return true;
                    break;

                // Display the Karma Log link on the sidebar
                case 'backend_sidebar_entries':
?>
<li class="serendipitySideBarMenuLink serendipitySideBarMenuEntryLinks">
    <a href="?serendipity[adminModule]=event_display&amp;serendipity[adminAction]=karmalog">
        <?php echo PLUGIN_KARMA_DISPLAY_LOG; ?>
    </a>
</li>
<?php
                    return true;
                    break;

                // Display the Karma Log!
                //case 'external_plugin':
                case 'backend_sidebar_entries_event_display_karmalog':
                    // Print any stored messages
                    //foreach ($serendipity['karma_messages'] as $msg) {
                    //    print("<div class='serendipityAdminInfo'>$msg</div>\n");
                    //}
                    // Was I asked to process any votes?
                    if (($serendipity['POST']['delete_button'] || $serendipity['POST']['approve_button'])
                        && sizeof($serendipity['POST']['delete']) != 0 && serendipity_checkFormToken()) {
                        foreach($serendipity['POST']['delete'] as $d => $i) {
                            $kdata = $serendipity['POST']['karmalog'.$i];
                            // validate posted variables 
                            // posted points
                            $ppoints = $kdata['points'];
                            if (!is_numeric($ppoints) || ((int)$ppoints < -2) || ((int)$ppoints > 2)) {
                                print("<div class='serendipityAdminMsgError'>".PLUGIN_KARMA_INVALID_INPUT."</div>\n");
                                return false;
                            }
                            // posted id
                            $pid = $kdata['entryid'];
                            if (!is_numeric($pid)) {
                                print("<div class='serendipityAdminMsgError'>".PLUGIN_KARMA_INVALID_INPUT."</div>\n");
                                return false;
                            }
                            // posted IP
                            $pip = long2ip(ip2long($kdata['ip']));
                            if ($pip == -1 || $pip === FALSE) {
                                print("<div class='serendipityAdminMsgError'>".PLUGIN_KARMA_INVALID_INPUT."</div>\n");
                                return false;
                            }
                            // posted user agent (need a better validator, I think)
                            $puser_agent = $kdata['user_agent'];
                            if (serendipity_db_escape_string($puser_agent) != $puser_agent) {
                                print("<div class='serendipityAdminMsgError'>".PLUGIN_KARMA_INVALID_INPUT."</div>\n");
                                return false;
                            }
                            // posted vote time
                            $pvotetime = $kdata['votetime'];
                            $unixsecs = date('U', $kdata['votetime']);
                            if ($pvotetime != $unixsecs) {
                                print("<div class='serendipityAdminMsgError'>".PLUGIN_KARMA_INVALID_INPUT."</div>\n");
                                return false;
                            }

                            // Remove karma from entry?
                            if ($serendipity['POST']['delete_button']) {
                                // Fetch vote total for the entry IDs
                                $q = 'SELECT k.*
                                    FROM ' . $serendipity['dbPrefix'] . 'karma   AS k
                                    WHERE k.entryid IN (' . $pid . ') GROUP BY k.entryid';

                                $sql = serendipity_db_query($q);
                                if (is_array($sql)) {
                                    $karma = $sql[0];

                                    $update = sprintf(
                                        "UPDATE {$serendipity['dbPrefix']}karma
                                        SET points   = %s,
                                        votes    = %s
                                        WHERE entryid  = %s",
                                        serendipity_db_escape_string($karma['points'] - $ppoints),
                                        serendipity_db_escape_string($karma['votes']  - 1),
                                        serendipity_db_escape_string($pid)
                                        );
                                    $updated = serendipity_db_query($update);
                                    if ($updated != 1) {
                                        printf("<div class='serendipityAdminMsgError'>".PLUGIN_KARMA_REMOVE_ERROR."</div>\n", $pid);
                                        // Don't delete from karma log if we couldn't take away the points
                                        continue;
                                    }
                                } else {
                                    // This will only happen if someone is messing with the karma table or submit data
                                    printf("<div class='serendipityAdminMsgError'>".PLUGIN_KARMA_UPDATE_ERROR."</div>", $pid);
                                    continue;
                                }
                            }

                            // Remove vote from log (approved or deleted, doesn't matter)
                            $del = sprintf(
                                "DELETE FROM {$serendipity['dbPrefix']}karmalog
                                WHERE entryid = %s AND ip = '%s' AND user_agent LIKE '%%%s%%' AND votetime = %s LIMIT 1",
                                serendipity_db_escape_string($pid),
                                serendipity_db_escape_string($pip),
                                serendipity_db_escape_string($puser_agent),
                                serendipity_db_escape_string($pvotetime)
                                );
                            $deleted = serendipity_db_query($del);
                            // User feedback
                            if ($deleted == 1) {
                                if ($serendipity['POST']['delete_button']) {
                                    printf("<div class='serendipityAdminMsgSuccess'>".PLUGIN_KARMA_REMOVED_POINTS."</div>\n", $ppoints, $pid);
                                } else {
                                    printf("<div class='serendipityAdminMsgSuccess'>".PLUGIN_KARMA_APPROVED_POINTS."</div>\n", $ppoints, $pid);
                                }
                            } else {
                                printf("<div class='serendipityAdminMsgError'>".PLUGIN_KARMA_REMOVE_ERROR."</div>\n", $pid);
                            }
                        }
                    }

                    // URL; expected to be event_display and karmalog, respectively 
                    $url = '?serendipity[adminModule]='.$serendipity['GET']['adminModule'].'&serendipity[adminAction]='.$serendipity['GET']['adminAction'];

                    // Filters
                    print("
<form action='' method='get' name='karmafilters' id='karmafilters'>
  <input type='hidden' name='serendipity[adminModule]' value='{$serendipity['GET']['adminModule']}' />
  <input type='hidden' name='serendipity[adminAction]' value='{$serendipity['GET']['adminAction']}' />
<table class='serendipity_admin_filters' width='100%'>
<tr>
  <td colspan='4' class='serendipity_admin_filters_headline'><strong>".FILTERS."</strong></td>
</tr>
<tr>
  <td>User Agent:</td>
  <td><input class='input_textbox' type='text' name='serendipity[filter][user_agent]' size='15' value='".htmlspecialchars($serendipity['GET']['filter']['user_agent'])."' /></td>
  <td>".IP."</td>
  <td><input class='input_textbox' type='text' name='serendipity[filter][ip]' size='15' value='".htmlspecialchars($serendipity['GET']['filter']['ip'])."' /></td>
</tr>
<tr>
  <td>Entry ID:</td>
  <td><input class='input_textbox' type='text' name='serendipity[filter][entryid]' size='15' value='".htmlspecialchars($serendipity['GET']['filter']['entryid'])."' /></td>
  <td>Entry title:</td>
  <td><input class='input_textbox' type='text' name='serendipity[filter][title]' size='30' value='".htmlspecialchars($serendipity['GET']['filter']['title'])."' /></td>
</tr>
</table>
");
                    // Set all filters into $and and $searchString
                    if (!empty($serendipity['GET']['filter']['entryid'])) {
                        $val = $serendipity['GET']['filter']['entryid'];
                        $and .= "AND l.entryid = '" . serendipity_db_escape_string($val) . "'";
                        $searchString .= "&amp;serendipity['filter']['entryid']=".htmlspecialchars($val);
                    }
                    if (!empty($serendipity['GET']['filter']['ip'])) {
                        $val = $serendipity['GET']['filter']['ip'];
                        $and .= "AND l.ip = '" . serendipity_db_escape_string($val) . "'";
                        $searchString .= "&amp;serendipity['filter']['ip']=".htmlspecialchars($val);
                    }
                    if (!empty($serendipity['GET']['filter']['user_agent'])) {
                        $val = $serendipity['GET']['filter']['user_agent'];
                        $and .= "AND l.user_agent LIKE '%" . serendipity_db_escape_string($val) . "%'";
                        $searchString .= "&amp;serendipity['filter']['user_agent']=".htmlspecialchars($val);
                    }
                    if (!empty($serendipity['GET']['filter']['title'])) {
                        $val = $serendipity['GET']['filter']['title'];
                        $and .= "AND e.title LIKE '%" . serendipity_db_escape_string($val) . "%'";
                        $searchString .= "&amp;serendipity['filter']['title']=".htmlspecialchars($val);
                    }

                    // Sorting (controls go after filtering controls in form above)
                    $sort_order = array(
                        'votetime' => DATE,
                        'user_agent' => USER_AGENT,
                        'title' => TITLE,
                        'entryid' => 'ID');
                    if (empty($serendipity['GET']['sort']['ordermode']) || $serendipity['GET']['sort']['ordermode'] != 'ASC') {
                        $desc = true;
                        $serendipity['GET']['sort']['ordermode'] = 'DESC';
                    }
                    if (!empty($serendipity['GET']['sort']['order']) && !empty($sort_order[$serendipity['GET']['sort']['order']])) {
                        $curr_order = $serendipity['GET']['sort']['order'];
                        $orderby = serendipity_db_escape_string($curr_order . ' ' . $serendipity['GET']['sort']['ordermode']);
                    } else {
                        $curr_order = 'votetime';
                        $orderby = 'votetime ' . serendipity_db_escape_string($serendipity['GET']['sort']['ordermode']);
                    }
                    print("
<div>".SORT_BY."
<select name='serendipity[sort][order]'>
");
                    foreach($sort_order as $order => $val) {
                        print("
  <option value='$order'".($curr_order == $order?" selected='selected'":'').">$val</option>
");
                    }
                    print("
</select>
<select name='serendipity[sort][ordermode]'>
  <option value='DESC'".($desc?"selected='selected'":'').">".SORT_ORDER_DESC."</option>
  <option value='ASC'".($desc?'':"selected='selected'").">".SORT_ORDER_ASC."</option>
</select>
</div>
<input type='submit' name='submit' value=' - ".GO." - ' class='serendipityPrettyButton input_button' /> 
</form>
");

                    // Paging (partly ripped from include/admin/comments.inc.php)
                    $commentsPerPage = (int)(!empty($serendipity['GET']['filter']['perpage']) ? $serendipity['GET']['filter']['perpage'] : 25);
                    $sql = serendipity_db_query("SELECT COUNT(*) AS total FROM {$serendipity['dbPrefix']}karmalog l WHERE 1 = 1 " . $and, true);
                    $totalVotes = $sql['total'];
                    $pages = ($commentsPerPage == COMMENTS_FILTER_ALL ? 1 : ceil($totalVotes/(int)$commentsPerPage));
                    $page = (int)$serendipity['GET']['page'];
                    if ($page == 0 || ($page > $pages)) {
                        $page = 1;
                    }

                    if ($page > 1) {
                        $linkPrevious = $url . '&amp;serendipity[page]='. ($page-1) . $searchString;
                    }
                    if ($pages > $page) {
                        $linkNext = $url . '&amp;serendipity[page]='. ($page+1) . $searchString;
                    }

                    if ($commentsPerPage == COMMENTS_FILTER_ALL) {
                        $limit = '';
                    }else {
                        $limit = serendipity_db_limit_sql(serendipity_db_limit(($page-1)*(int)$commentsPerPage, (int)$commentsPerPage));
                    }

                    // Variables for display
                    if ($linkPrevious) {
                        $linkPrevious = '<a href="' . $linkPrevious . '" class="serendipityIconLink"><img src="'.serendipity_getTemplateFile('admin/img/previous.png').'" /></a>';
                    } else {
                        $linkPrevious = '&nbsp;';
                    }
                    if ($linkNext) {
                        $linkNext = '<a href="' . $linkNext . '" class="serendipityIconLinkRight"><img src="'.serendipity_getTemplateFile('admin/img/next.png').'" /></a>';
                    } else {
                        $linkNext = '&nbsp;';
                    }
                    $paging = sprintf(PAGE_BROWSE_COMMENTS, $page, $pages, $totalVotes);

                    // Retrieve the next batch of karma votes
                    // [entryid, points, ip, user_agent, votetime]
                    $sql = serendipity_db_query("SELECT l.entryid AS entryid, l.points AS points, l.ip AS ip, l.user_agent AS user_agent, l.votetime AS votetime, e.title AS title FROM {$serendipity['dbPrefix']}karmalog l
                        LEFT JOIN {$serendipity['dbPrefix']}entries e ON (e.id = l.entryid)
                        WHERE 1 = 1 " . $and . "
                        ORDER BY $orderby $limit");

                    // Start the form for display and deleting
                    if (is_array($sql)) {
                        print("<form action='' method='post' name='formMultiDelete' id='formMultiDelete'>\n".serendipity_setFormToken()."
<script type='text/javascript'>
function invertSelection() {
    var f = document.formMultiDelete;
    for (var i = 0; i < f.elements.length; i++) {
        if( f.elements[i].type == 'checkbox' ) {
            f.elements[i].checked = !(f.elements[i].checked);
        }
    }
}
</script>
");

                        // Print the header paging table
                        print("
<table width='100%' style='border-collapse: collapse;'>
  <tr>
  <td align='left'>$linkPrevious</td>
  <td align='center'>$paging</td>
  <td align='right'>$linkNext</td>
  </tr>
</table>
");
                        // Start the vote table
                        print("
<table class='karmalog' width='100%'>
");
                        // Print each vote
                        $i = 0;
                        foreach ($sql as $vote) {
                            $i++;
                            // entryid, title, points, ip, user_agent, votetime
                            $entrylink = serendipity_archiveURL($vote['entryid'], $vote['title'], 'serendipityHTTPPath', true);
                            $entryFilterHtml = "<a class='serendipityIconLink' href='$url&serendipity[filter][entryid]={$vote['entryid']}'><img src='".serendipity_getTemplateFile('admin/img/zoom.png')."' /></a>";
                            $ipFilterHtml = "<a class='serendipityIconLink' href='$url&serendipity[filter][ip]={$vote['ip']}'><img src='".serendipity_getTemplateFile('admin/img/zoom.png')."' /></a>";
                            $timestr = strftime('%H:%M:%S<br />%n%a %b %d %Y', $vote['votetime']);
                            $cssClass = 'serendipity_admin_list_item serendipity_admin_list_item_';
                            $cssClass .= (($i % 2 ==0)?'even':'uneven');
                            $barClass = str_replace(array('.',' '), array('_','_'), $this->image_name);
                            $barHtml = $this->createRatingBar(null, $vote['points'], 1, $barClass);
                            $barHtml = sprintf($barHtml, 'what', $vote['points'], '1');
                            print("
  <tr class='$cssClass'>
    <td rowspan='2' width='20' align='center'>
      <input class='input_checkbox' type='checkbox' name='serendipity[delete][$i]' value='$i' tabindex='$i' />
      <input type='hidden' name='serendipity[karmalog$i][points]' value='{$vote['points']}' />
      <input type='hidden' name='serendipity[karmalog$i][entryid]' value='{$vote['entryid']}' />
      <input type='hidden' name='serendipity[karmalog$i][votetime]' value='{$vote['votetime']}' />
      <input type='hidden' name='serendipity[karmalog$i][ip]' value='{$vote['ip']}' />
      <input type='hidden' name='serendipity[karmalog$i][user_agent]' value='{$vote['user_agent']}' />
    </td>
    <td>$barHtml</td>
    <td colspan='2'><a href='$entrylink' title='{$vote['entryid']}' alt='{$vote['title']}'>{$vote['title']}</a> $entryFilterHtml</td>
  </tr>
  <tr class='$cssClass'>
    <td>$timestr</td>
    <td>{$vote['ip']} $ipFilterHtml</td>
    <td>{$vote['user_agent']}</td>
  </tr>
");
                        }

                        // End the vote table
                        print("
                            </table>
                            ");
                        if (is_array($sql)) {
                            print("
<input type='button' name='toggle' value='".INVERT_SELECTIONS."' onclick='invertSelection()' class='serendipityPrettyButton input_button' /> 
<input class='serendipityPrettyButton input_button' type='submit' value='" . PLUGIN_KARMA_DELETE_VOTES . "' name='serendipity[delete_button]' />
<input class='serendipityPrettyButton input_button' type='submit' value='" . PLUGIN_KARMA_APPROVE_VOTES . "' name='serendipity[approve_button]' />
</form>
");
                        }

                        // Print the footer paging table
                        print("
<table width='100%' style='border-collapse: collapse;'>
  <tr>
  <td align='left'>$linkPrevious</td>
  <td align='center'>$paging</td>
  <td align='right'>$linkNext</td>
  </tr>
</table>
");
                    } else {
                        print("
<div class='serendipityAdminMsgNotice'>No entries to display.</div>
");
                    }

                    return true;
                    break;

                default:
                    return false;
            }// End switch on event hooks
        } else {
            return false;
        }
    }
    
    /**
     * Check, if visit counting for the actual visitor should be done.
     */
    function track_clicks_allowed_by_user(){
        if (!$this->get_config('track_visits_of_loggedin_users',true) && serendipity_userLoggedIn()){
            return false;
        }
        return true;
    }

    /**
     * Creates an HTML snippet with images of all the available rating bars.
     *
     * @return string an HTML string including working images of all the rating bars in the img/ directory.
     */
    function createRatingSelector() {
        // Since the inputs are set up with the proper names, the config item
        // gets saved automatically, with no need for magic

        // Get the filename to be automatically selected
        $this->set_valid_image_data();
        $cursel = $this->image_name;

        $this->select_css = '';
        $this->select_html = '';
        // We will be wrapped in a <tr><td colspan="2">
        $this->select_html .= "
<strong>" . PLUGIN_KARMA_IMAGE . "</strong><br />
<span style='color: rgb(94, 122, 148); font-size: 8pt;'>&nbsp;".PLUGIN_KARMA_IMAGE_DESC."</span>
</td>
<td></td>
</tr>
<tr>
<td colspan='2'>
<table border='1' class='serendipity_karmaVote_selectorTable'>";
        // Add the 'text-only' selection and its CSS
        if ($cursel == '0') {
            $checked = 'checked="checked" ';
        } else {
            $checked = '';
        }
        $this->image_name = '0';
        $bar = $this->createRatingBar('', 0, 0, 'textbar');
        $this->select_html .= "
<tr id='serendipity_karmaVote_selectorTable_textOnly'>
<td colspan='3' align='center'><input type='radio' name='serendipity[plugin][base_image]' value='0' $checked/>" . PLUGIN_KARMA_STATISTICS_POINTS_NO . "<br />$bar<br /></td>\n";
        $this->select_css .= "
.textbar, .textbar a, .textbar a:hover {
    font-size: 100%;
    position: relative;
    background: none;
}
.serendipityAdminContent span.textbar {
    color: black !important;
}
";
        // Retrieve all the *valid* images from the image directory
        $files = $this->getImageFiles();
        // Add an <ol> for each rating bar, and add its CSS overrides 
        $n = 0;
        foreach ($files as $fdata) {
            // Columnize 
            if (($n % 3) == 0) {
                // Time to start a new row
                $this->select_html .= "</tr>\n<tr>\n";
            }

            // Set the image data
            $fname = $fdata['fname'];
            $height = $fdata['height'];
            $width = $fdata['width'];
            $ratio = $width / $height;
            // If this is a single segment, adjust width
            if ($ratio < $max_segment_ratio) {
                $width = $width * 5;
            }
            $height = $height / 3;
            // Set up class variables correctly
            $this->image_name = $fname;
            $this->image_width = $width;
            $this->image_height = $height;

            // Create a rating bar of this image
            //
            // What would be a good CSS class for this image?
            $css_class = str_replace(array('.',' '), array('_','_'), $fname);
            $checked = '';
            if ($fname == $cursel) {
                $checked = 'checked="checked" ';
            }
            $bar_html = 
"<td align='center' id='serendipity_karmaVote_select_$css_class'>
    <input type='radio' name='serendipity[plugin][base_image]' value='$fname' $checked/>
    <span style='font-size: 8pt;'>$fname</span><br />\n" . 
                $this->createRatingBar('', -1, 2, $css_class) .
"</td>\n";
            $bar_html = sprintf($bar_html, '', '2.5 of 5', '1');
            $this->select_html .= $bar_html;
            // Add the necessary CSS to the stylesheet (will be added when css hooks are called)
            // Sorry to interrupt your regularly scheduled HTML; I need to 
            // use the $css_class while it's still here.
            $this->select_css .= "
/* Overrides for $css_class */
.$css_class 
{
  width: ${width}px;
  height: ${height}px;
}
.$css_class,
.$css_class a:hover,
.$css_class .serendipity_karmaVoting_current-rating
{
  background-image: url({$serendipity['baseURL']}plugins/serendipity_event_karma/img/${fname});
}
.$css_class,
.$css_class a,
.$css_class .serendipity_karmaVoting_current-rating
{
  line-height: ${height}px;
  height: ${height}px;
}

";
            $n++;
        } // Go back up for another image

        // Check for nothing displayed
        if ($n == 0) {
            // There were no images!
            $this->select_html .= "</tr>\n<tr><td>" . PLUGIN_KARMA_NO_IMAGES . "</td>";
        }

        // End the table, with a config-item bottom-border separator
        $this->select_html .= 
"</tr>\n</table>
<tr><td colspan='2' style='border-bottom: 1px solid #000000; vertical-align: top'>&nbsp;<td></tr>\n";
        // The config item and row are closed by the core code

        return $this->select_html;
    }

    /**
     * Retrieves all the images from the img/ directory
     *
     * @return array an array with 'fname', 'width', and 'height' entries for
     *     each valid image file in the img/ directory
     */
    function getImageFiles() {
        $path = dirname(__FILE__) . "/img";
        $images = array();
        $folder = opendir($path);
        while (false !== ($filename = readdir($folder))) {
            $parts = serendipity_parseFileName($filename);
            $img_data = serendipity_getimagesize($path . '/' . $filename);
            if (!isset($img_data['noimage'])) {
                // Curly braces are just a different syntax of associative array assignment
                $images{$filename} = array('fname'=>$filename, 'width'=>$img_data[0], 'height'=>$img_data[1]);
            }
        }
        closedir($folder);                               
        ksort($images);
        return $images;
    }

    /**
     * Creates the HTML snippet for the currently defined rating bar, with 
     * appropriate links (and current rating indication for graphical bars).
     * Automatically detects from $this->image_name whether to create text
     * links or a graphical bar.
     *
     * @param string id optional The ID of the bar we're creating; an empty
     *     id ('' or 0) creates a bar with dummy links; null creates a bar with 
     *     no voting links at all (shows only current configuration).
     * @param int karma optional The current total karma (default: 0 points)
     * @param int votes optional The total number of votes (default: 0 votes))
     * @param string extra_class optional Any additional CSS classes to be added to the bar container
     */
    function createRatingBar($id = null, $karma = 0, $votes = 0, $extra_class = '') {
        // Are there enough votes to display the current rating?
        $enough_votes = ($votes >= $this->get_config('min_disp_votes', 0));
        // Do I need to create links?
        if ($id !== null) {
            // Create the rating tooltips and texts
            $vilestr = $this->get_config('rate_vile', PLUGIN_KARMA_VOTEPOINT_1);
            $poorstr = $this->get_config('rate_poor', PLUGIN_KARMA_VOTEPOINT_2);
            $okaystr = $this->get_config('rate_okay', PLUGIN_KARMA_VOTEPOINT_3);
            $goodstr = $this->get_config('rate_good', PLUGIN_KARMA_VOTEPOINT_4);
            $beststr = $this->get_config('rate_best', PLUGIN_KARMA_VOTEPOINT_5);
            $vile = sprintf(PLUGIN_KARMA_RATE, $vilestr);
            $poor = sprintf(PLUGIN_KARMA_RATE, $poorstr);
            $okay = sprintf(PLUGIN_KARMA_RATE, $okaystr);
            $good = sprintf(PLUGIN_KARMA_RATE, $goodstr);
            $best = sprintf(PLUGIN_KARMA_RATE, $beststr);
            $vilestr = $this->get_config('text_vile', PLUGIN_KARMA_VOTETEXT_1);
            $poorstr = $this->get_config('text_poor', PLUGIN_KARMA_VOTETEXT_2);
            $okaystr = $this->get_config('text_okay', PLUGIN_KARMA_VOTETEXT_3);
            $goodstr = $this->get_config('text_good', PLUGIN_KARMA_VOTETEXT_4);
            $beststr = $this->get_config('text_best', PLUGIN_KARMA_VOTETEXT_5);
            
            if (empty($id)) {
                // Create dummy links
                $link_1 = 
'<a class="serendipity_karmaVoting_link1" rel="nofollow" href="#" title="' . $vile . '">' . $vilestr . '</a>';
                $link_2 = 
'<a class="serendipity_karmaVoting_link2" rel="nofollow" href="#" title="' . $poor . '">' . $poorstr . '</a>';
                $link_3 = 
'<a class="serendipity_karmaVoting_link3" rel="nofollow" href="#" title="' . $okay . '">' . $okaystr . '</a>';
                $link_4 = 
'<a class="serendipity_karmaVoting_link4" rel="nofollow" href="#" title="' . $good . '">' . $goodstr . '</a>';
                $link_5 = 
'<a class="serendipity_karmaVoting_link5" rel="nofollow" href="#" title="' . $best . '">' . $beststr . '</a>';
                // There *are* enough votes for a dummy rating bar
                $enough_votes = true;
            } else {
                // Create ordinary links
                $link_1 = "<a class='serendipity_karmaVoting_link1' rel='nofollow' href='%5\$sserendipity[karmaVote]=-2&amp;serendipity[karmaId]=$id#karma_vote$id' title='$vile'>$vilestr</a>";
                $link_2 = "<a class='serendipity_karmaVoting_link2' rel='nofollow' href='%5\$sserendipity[karmaVote]=-1&amp;serendipity[karmaId]=$id#karma_vote$id' title='$poor'>$poorstr</a>";
                $link_3 = "<a class='serendipity_karmaVoting_link3' rel='nofollow' href='%5\$sserendipity[karmaVote]=0&amp;serendipity[karmaId]=$id#karma_vote$id' title='$okay'> $okaystr</a>";
                $link_4 = "<a class='serendipity_karmaVoting_link4' rel='nofollow' href='%5\$sserendipity[karmaVote]=1&amp;serendipity[karmaId]=$id#karma_vote$id' title='$good'>$goodstr</a>";
                $link_5 = "<a class='serendipity_karmaVoting_link5' rel='nofollow' href='%5\$sserendipity[karmaVote]=2&amp;serendipity[karmaId]=$id#karma_vote$id' title='$best'>$beststr</a>";
            }
        }

        // Normalize the CSS class
        if (!empty($extra_class)) {
            $extra_class = " $extra_class";
        }

        // Create the rating bar
        if ($this->image_name == '0') {
            // Text bar
            //
            if ($id === null) {
                // No need for a "current rating" bar, like with graphics
                $karma_display = '';
            } else {
                // Show the links
                $karma_display = "
                    <span class='serendipity_karmaVoting_links$extra_class'>$link_1 | $link_2 | $link_3 | $link_4 | $link_5</span><br />";
            }
        } else {
            // Graphic bar
            //

            // No need to create a bar if there's nothing to display
            if (($id !== null) || $enough_votes) {
                // Start the bar
                $karma_display = "
<ol class='serendipity_karmaVoting_links$extra_class'>";
                // Only display the current rating if there are enough votes
                if ($enough_votes) {
                    // Figure out the image-based rating and width
                    $rating = $this->imageRating($karma, $votes);
                    $cr_width = ($rating * $this->image_width) / 5;
                    // Get current karma text
                    $curr_msg = $this->get_config('curr_msg', PLUGIN_KARMA_CURRENT);
                    $karma_display .= "
    <li class='serendipity_karmaVoting_current-rating' style='width: ${cr_width}px;' title='$curr_msg'> </li>
    ";
                }
                // Only create voting links if required
                if ($id !== null) {
                    $karma_display .= "
    <li>$link_1</li>
    <li>$link_2</li>
    <li>$link_3</li>
    <li>$link_4</li>
    <li>$link_5</li>
    ";
                }
                // Close the <ol>
                $karma_display .= "
</ol>
";
            }
        }
        return $karma_display;
    }

    /**
     * Sets $this->image_name to the name of a valid image, or to '0' to
     * indicate text-only rating bars should be used.
     */
    function set_valid_image_data() {
        $base_image = $this->get_config('base_image', false);
        if ($base_image !== false) {
            // Definitely configured
            if ($base_image == '0') {
                // Configured to text-only
                $this->image_name = $base_image;
            } else {
                $imagesize = serendipity_getimagesize(dirname(__FILE__) . "/img/" . $base_image);
                if ($imagesize['noimage']) {
                    // Leave as default
                } else {
                    // Set to valid image name
                    $this->image_name = $base_image;
                }
            }
        }
        // Is the (possibly default) image valid?
        if ($this->image_name) {
            $imagesize = serendipity_getimagesize(dirname(__FILE__) . "/img/" . $this->image_name);
            if ($imagesize['noimage']) {
                // No valid image; use text-only
                $this->image_name = '0';
            } else {
                // Valid graphical image; set the dimensions, too
                $this->image_width = $imagesize[0];
                $this->image_height = $imagesize[1];
            }
        }
    }

    /**
     * Compute the equivalent graphical points rating for a karma rating.
     *
     * @param int points The total karma points
     * @param int votes The total number of votes
     * 
     * @return string A string indicating the number of points "of 5", 
     *     or PLUGIN_KARMA_IMAGE_NONE_RATING if no votes have been recorded.
     */
    function imageRating($points, $votes) {
        if ($votes == 0) {
            $rating = PLUGIN_KARMA_IMAGE_NONE_RATING;
        } else {
            // Find average karma (-2 to +2)
            $rating = ((float)$points) / ((float)$votes);
            // Remap into [1,5]
            $rating = $rating + 3;
            // Put it into the language-specific string format
            if ($rating == (int)$rating) {
                $rating = sprintf(PLUGIN_KARMA_IMAGE_INT_RATING, $rating);
            } else {
                $rating = sprintf(PLUGIN_KARMA_IMAGE_RATING, $rating);
            }
        }
        return $rating;
    }

    /* Compute the equivalent word rating for a karma rating.
     * 
     * @param mixed points The total karma points
     * @param mixed votes The total number of votes
     *
     * @return string A word corresponding to the article rating, 
     *     or PLUGIN_KARMA_IMAGE_NONE_RATING if no votes have been recorded.
     */
    function wordRating($points, $votes) {
        if ($votes == 0) {
            $rating = PLUGIN_KARMA_IMAGE_NONE_RATING;
        } else {
            // Find average karma (-2 to +2)
            $rating = ((float)$points) / ((float)$votes);
            // Put it into the language-specific string format, rounding up
            // I'm mapping invalid cases (rating > 5 and rating < -2) to the extremes
            if ($rating <= -1.5) {
                $rating = PLUGIN_KARMA_VOTEPOINT_1;
            } elseif ($rating <= -0.5) {
                $rating = PLUGIN_KARMA_VOTEPOINT_2;
            } elseif ($rating <= 0.5) {
            } elseif ($rating <= 1.5) {
                $rating = PLUGIN_KARMA_VOTEPOINT_4;
            } else {
                $rating = PLUGIN_KARMA_VOTEPOINT_5;
            }
        }
        return $rating;
    }
}

/* vim: set sts=4 ts=4 sw=4 expandtab : */
