Error: serendipity_plugin_popular

Creating and modifying plugins.
Post Reply
serendipity-fan
Regular
Posts: 7
Joined: Fri Oct 03, 2008 11:02 am

Error: serendipity_plugin_popular

Post by serendipity-fan »

Cheers everyone,

I've written a plugin that records and displays clicks and access times to blog entries. The plugin is named serendipity_plugin_popular or respectively serendipity_event_popular. Works fine on my development machine. When I try to install it on the production server I get this message:

Error: serendipity_plugin_popular

which appears on top of the Sidebar Plugins page (the one with all the available plugins) and of course the plugin is not listed. I have written a unit test which runs fine for both plugins on the production sever. Class can be loaded, objects can be created, and all methods can be called. I haven't got the foggiest idea what went wrong and Serendipity doesn't seem to keep an error log, or does it?

Any suggestions?

Greetings from Thailand!
Serendipity Fan
garvinhicking
Core Developer
Posts: 30022
Joined: Tue Sep 16, 2003 9:45 pm
Location: Cologne, Germany
Contact:

Re: Error: serendipity_plugin_popular

Post by garvinhicking »

Hi!

Usually the only reason why this could happen if the PHP file of that plugin contains a PHP Parse Error,so that s9y cannot instantiate that file.

Do you maybe havedifferent PHP versions on your two servers, so that it would work on one server but not the other?

Maybe you can show your code of that serendipitY_plugin_popular.php?

Regards,
Garvin
# Garvin Hicking (s9y Developer)
# Did I help you? Consider making me happy: http://wishes.garv.in/
# or use my PayPal account "paypal {at} supergarv (dot) de"
# My "other" hobby: http://flickr.garv.in/
serendipity-fan
Regular
Posts: 7
Joined: Fri Oct 03, 2008 11:02 am

Post by serendipity-fan »

Hi Garvin,

Must be my lucky day seeing you respond to my question right away. Many thanks! The two servers run indeed two different PHP versions. My development machine runs 5.2.4 and the production server runs 4.3.10. I suspected exactly the same thing, a parse error with 4.x, however, the unit test runs okay on the production server. So, I guess it can't be. I've tested it with Serendipity V-1.3 and V-1.3.1. Here are the sources:

serendipty_plugin_popular.php

Code: Select all

<?php # $Id: serendipity_plugin_popular.php,v 1.0 2008/09/27 11:31:55 time4you Exp $

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_POPULAR_DEFAULT_NUMBER_OF_ENTRIES', 5);


class serendipity_plugin_popular extends serendipity_plugin {
    var $title = PLUGIN_POPULAR_TITLE;

    function introspect(&$propbag) {
        $this->title = $this->get_config('title', $this->title);

        $propbag->add('name', PLUGIN_POPULAR_TITLE);
        $propbag->add('description', PLUGIN_POPULAR_BLAHBLAH);
        $propbag->add('stackable', true);
        $propbag->add('author', 'time4you GmbH');
        $propbag->add('requirements',  array(
            'serendipity' => '0.9',
            'smarty'      => '2.6.7',
            'php'         => '4.1.0'
        ));
        $propbag->add('version', '1.0');
        $propbag->add('configuration', array('title', 
            'mostrecent', 'mostrecent_number',
            'mostread', 'mostread_number',
            'lastread', 'lastread_number',
            'mostcommented', 'mostcommented_number',
            'lastcommented', 'lastcommented_number',
        ));
        $propbag->add('groups', array('STATISTICS'));
    }


    function introspect_config_item($name, &$propbag) {
        global $serendipity;
        
        switch($name) {
            case 'title':
                $propbag->add('type', 'string');
                $propbag->add('name', TITLE);
                $propbag->add('description', TITLE_FOR_NUGGET);
                $propbag->add('default', PLUGIN_POPULAR_TITLE);
                break;
            case 'mostrecent':
                $propbag->add('type', 'boolean');
                $propbag->add('name', PLUGIN_POPULAR_MOSTRECENT);
                $propbag->add('description', PLUGIN_POPULAR_MOSTRECENT_BLAHBLAH);
                $propbag->add('default', true);
                break;
            case 'mostread':
                $propbag->add('type', 'boolean');
                $propbag->add('name', PLUGIN_POPULAR_MOSTREAD);
                $propbag->add('description', PLUGIN_POPULAR_MOSTREAD_BLAHBLAH);
                $propbag->add('default', true);
                break;
            case 'lastread':
                $propbag->add('type', 'boolean');
                $propbag->add('name', PLUGIN_POPULAR_LASTREAD);
                $propbag->add('description', PLUGIN_POPULAR_LASTREAD_BLAHBLAH);
                $propbag->add('default', false);
                break;
            case 'mostcommented':
                $propbag->add('type', 'boolean');
                $propbag->add('name', PLUGIN_POPULAR_MOSTCOMMENTED);
                $propbag->add('description', PLUGIN_POPULAR_MOSTCOMMENTED_BLAHBLAH);
                $propbag->add('default', true);
                break;
            case 'lastcommented':
                $propbag->add('type', 'boolean');
                $propbag->add('name', PLUGIN_POPULAR_LASTCOMMENTED);
                $propbag->add('description', PLUGIN_POPULAR_LASTCOMMENTED_BLAHBLAH);
                $propbag->add('default', false);
                break;
            case 'mostrecent_number':
            case 'mostread_number':
            case 'lastread_number':
            case 'mostcommented_number':
            case 'lastcommented_number':
                $propbag->add('type', 'string');
                $propbag->add('name', PLUGIN_POPULAR_NUMBER);
                $propbag->add('description', PLUGIN_POPULAR_NUMBER_BLAHBLAH);
                $propbag->add('validate', 'number');
                $propbag->add('validate_error', PLUGIN_POPULAR_NUMBER_ERRORTEXT);
                $propbag->add('default', PLUGIN_POPULAR_DEFAULT_NUMBER_OF_ENTRIES);
                break;
            default:
                return false;
        }
        return true;
    }


    function generate_content(&$title) {
        global $serendipity;
        $title = $this->get_config('title', $this->title);
        if ($this->get_config('mostrecent')) {
            $number = $this->get_number_of_links('mostrecent_number');
            $query = 
                "SELECT e.* 
                FROM {$serendipity['dbPrefix']}entries AS e
                WHERE e.isdraft = 'false' AND e.timestamp <= " . time() . "
                ORDER BY e.timestamp DESC
                LIMIT $number";
            $articles = serendipity_db_query($query);
            $this->output_links($articles, PLUGIN_POPULAR_MOSTRECENT);
        }
        if ($this->get_config('mostread')) {
            $number = $this->get_number_of_links('mostread_number');
            $query = 
                "SELECT DISTINCT e.* 
                FROM {$serendipity['dbPrefix']}entries AS e
                LEFT OUTER JOIN {$serendipity['dbPrefix']}plugin_popular AS p
                ON p.entry_id = e.id
                WHERE e.isdraft = 'false' AND e.timestamp <= " . time() . "
                ORDER BY p.count DESC
                LIMIT $number";
            $articles = serendipity_db_query($query);
            $this->output_links($articles, PLUGIN_POPULAR_MOSTREAD);
        }
        if ($this->get_config('lastread')) {
            $number = $this->get_number_of_links('lastread_number');
            $query = 
                "SELECT DISTINCT e.* 
                FROM {$serendipity['dbPrefix']}entries AS e
                LEFT OUTER JOIN {$serendipity['dbPrefix']}plugin_popular AS p
                ON p.entry_id = e.id
                WHERE e.isdraft = 'false' AND e.timestamp <= " . time() . "
                ORDER BY p.timestamp DESC
                LIMIT $number";
            $articles = serendipity_db_query($query);
            $this->output_links($articles, PLUGIN_POPULAR_LASTREAD);
        }
        if ($this->get_config('mostcommented')) {
            $number = $this->get_number_of_links('mostcommented_number');
            $query = 
                "SELECT e.* 
                FROM {$serendipity['dbPrefix']}entries AS e
                WHERE e.isdraft = 'false' AND e.timestamp <= " . time() . "
                AND e.comments > 0
                ORDER BY e.comments DESC
                LIMIT $number";
            $articles = serendipity_db_query($query);
            $this->output_links($articles, PLUGIN_POPULAR_MOSTCOMMENTED);
        }
        if ($this->get_config('lastcommented')) {
            $number = $this->get_number_of_links('lastcommented_number');
            $query = 
                "SELECT DISTINCT e.* 
                FROM {$serendipity['dbPrefix']}entries AS e
                LEFT OUTER JOIN {$serendipity['dbPrefix']}comments AS c
                ON c.entry_id = e.id
                WHERE e.isdraft = 'false' AND e.timestamp <= " . time() . "
                AND e.comments > 0
                ORDER BY c.timestamp DESC
                LIMIT $number";
            $articles = serendipity_db_query($query);
            $this->output_links($articles, PLUGIN_POPULAR_LASTCOMMENTED);
        }
    }


    function output_links(&$articles, $title) {
        if (isset($articles) && is_array($articles) && (count($articles) > 0) )  {
            echo htmlentities($title), "\n"; 
            echo '<div style="margin-left:10px;margin-bottom:5px">', "\n";
            foreach ($articles as $k => $article) {
                $articleLink = serendipity_archiveURL(
                    $article['id'],
                    $article['title'],
                    'serendipityHTTPPath',
                    true,
                    array('timestamp' => $article['timestamp'])
                );
                echo '<a href="', $articleLink, 
                     '" title="', htmlspecialchars($article['title']), '">', 
                     $article['title'], '</a><br />', "\n";
            }
            echo '</div>', "\n";
        }
    }


    function get_number_of_links($identifier) {
        $number = (int) $this->get_config($identifier, PLUGIN_POPULAR_DEFAULT_NUMBER_OF_ENTRIES);
        return ($number > 0)? $number : PLUGIN_POPULAR_DEFAULT_NUMBER_OF_ENTRIES;
    }


}
serendipity_event_popular.php:

Code: Select all

<?php # $Id: serendipity_event_popular.php,v 1.0 2008/09/27 11:31:55 time4you Exp $

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';

class serendipity_event_popular extends serendipity_event {
    var $title = PLUGIN_POPULAR_TITLE;


    function install() {
        global $serendipity;

        $sql = "CREATE TABLE {$serendipity['dbPrefix']}plugin_popular (
            entry_id int(10) unsigned default NULL,
            count int(10) unsigned default '0',
            timestamp int(10) unsigned default NULL,
            UNIQUE KEY entry_id (entry_id),
            KEY count (count),
            KEY timestamp (timestamp)
        );";
        serendipity_db_schema_import($sql);
    }


    function introspect(&$propbag) {
        $this->title = $this->get_config('title', $this->title);

        $propbag->add('name', PLUGIN_POPULAR_TITLE);
        $propbag->add('description', PLUGIN_POPULAR_REQUIREDBY);
        $propbag->add('stackable', false);
        $propbag->add('author', 'time4you GmbH');
        $propbag->add('requirements',  array(
            'serendipity' => '0.9',
            'smarty'      => '2.6.7',
            'php'         => '4.1.0'
        ));
        $propbag->add('version', '1.0');
        $propbag->add('groups', array('STATISTICS'));
        $propbag->add('event_hooks',   array(
            'frontend_display:html:per_entry' => true,
            'frontend_display:rss-1.0:per_entry' => true,
            'frontend_display:rss-2.0:per_entry' => true,
            'xmlrpc_fetchEntry' => true,
            'entry_display' => true 
        ));
    }


    function generate_content(&$title) {
        $title = $this->title;
    }


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

        $hooks = &$bag->get('event_hooks');
        if (!isset($hooks[$event]))
           return false; 
        switch($event) {
            case 'entry_display':
                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 ($entryid)
                    $this->increment_counter($entryid);
                return true;
            case 'frontend_display:html:per_entry':
            case 'frontend_display:rss-1.0:per_entry':
            case 'frontend_display:rss-2.0:per_entry':
            case 'xmlrpc_fetchEntry':
                if (isset($eventData['id'])) {
                   $entryid = (int) $eventData['id'];
                   $this->increment_counter($entryid, false);
                }
                return true;
            default:
               return false;
        }
        return false;
    }
    
    
    function increment_counter($entryid, $settime=true, $count=1) {
        global $serendipity;

        $sql = "UPDATE {$serendipity['dbPrefix']}plugin_popular 
            SET count = count + " . $count;
        if ($settime)
            $sql .=  ', timestamp = ' . time();  
        $sql .= " WHERE entry_id = $entryid"; 
        serendipity_db_query($sql, true);
        if (serendipity_db_affected_rows() < 1) {
            $sql = "INSERT INTO {$serendipity['dbPrefix']}plugin_popular 
                (entry_id, count, timestamp) 
                VALUES ('$entryid', '1', '" . time() . "')";
            serendipity_db_query($sql);
        }
    }
    
}
lang_en.inc.php:

Code: Select all

<?php # $Id: lang_en.inc.php,v 1.0 2008/09/27 11:31:55 time4you Exp $

/**
 *  @version $Revision: 1.0 $
 *  @author Thomas Knierim, time4you GmbH, www.time4you.de
 *  EN-Revision: Revision of lang_en.inc.php
 */

@define('PLUGIN_POPULAR_TITLE', 'Popular Articles');
@define('PLUGIN_POPULAR_BLAHBLAH', 'Shows links to recent and/or popular articles in the sidebar. To display the most read or the last read articles, the associated event plugin "Popular Articles" must be installed.');
@define('PLUGIN_POPULAR_REQUIREDBY', 'This plugin is required by the "Popular Articles" sidebar plugin.');
@define('PLUGIN_POPULAR_NUMBER', 'Number Of Articles');
@define('PLUGIN_POPULAR_NUMBER_BLAHBLAH', 'How many items should be displayed?');
@define('PLUGIN_POPULAR_NUMBER_ERRORTEXT', 'Number of items must be an integer number.');
@define('PLUGIN_POPULAR_MOSTRECENT', 'Most Recent');
@define('PLUGIN_POPULAR_MOSTRECENT_BLAHBLAH', 'Display the most recent articles?');
@define('PLUGIN_POPULAR_MOSTREAD', 'Most Read');
@define('PLUGIN_POPULAR_MOSTREAD_BLAHBLAH', 'Display the most read articles?');
@define('PLUGIN_POPULAR_LASTREAD', 'Last Read');
@define('PLUGIN_POPULAR_LASTREAD_BLAHBLAH', 'Display the last read articles?');
@define('PLUGIN_POPULAR_MOSTCOMMENTED', 'Most Commented');
@define('PLUGIN_POPULAR_MOSTCOMMENTED_BLAHBLAH', 'Display the most commented articles?');
@define('PLUGIN_POPULAR_LASTCOMMENTED', 'Last Commented');
@define('PLUGIN_POPULAR_LASTCOMMENTED_BLAHBLAH', 'Display the last commented articles?');

?>
Unit Test Code (test_plugin_popular.php):

Code: Select all

<?php

header('Content-type: text/plain');

define('IN_serendipity', true);

$serendipity = array();

class serendipity_plugin {
    function get_config($param1, $param2=0) {}
    function install() {}
    function introspect(&$propertybag) {}
    function introspect_config_item($param1, $param2) {}
    function generate_content(&$title) {}
}

class PropertyBag {
   var $contents = array();
   function add($param1, $param2) {
      $this->contents[$param1] = $param2; 
   }
   function getContents() {
      return $this->contents;
   }
}

class serendipity_event extends serendipity_plugin{}

echo "Start Test\n";
include 'serendipity_plugin_popular.php';
echo "serendipity_plugin_popular.php loaded!\n";
$plugin = new serendipity_plugin_popular();
echo "serendipity_plugin_popular object created!\n";
$propertybag = new PropertyBag();
$plugin->introspect(&$propertybag);
echo "Called introspect()!\n";
$plugin->introspect_config_item('title', &$propertybag);
echo "Called introspect_config_item()!\n";
echo "Result:\n";
var_dump($propertybag);
echo "Calling generate_content():\n";
$title = 'Title';
$plugin->generate_content(&$title);
echo "generate_content() finished!\n";
include 'serendipity_event_popular.php';
echo "serendipity_event_popular.php loaded!\n";
$plugin = new serendipity_event_popular();
echo "serendipity_event_popular object created!\n";
$propertybag = new PropertyBag();
$plugin->introspect(&$propertybag);
echo "Called introspect()!\n";
$plugin->introspect_config_item('title', &$propertybag);
echo "Called introspect_config_item()!\n";
echo "Result:\n";
var_dump($propertybag);
echo "Calling generate_content():\n";
$title = 'Title';
$plugin->generate_content(&$title);
echo "generate_content() finished!\n";
echo "End Test\n";

?>
Cheers, Serendipity-Fan
garvinhicking
Core Developer
Posts: 30022
Joined: Tue Sep 16, 2003 9:45 pm
Location: Cologne, Germany
Contact:

Post by garvinhicking »

Hi!

Too strange, the code looks good enough. Did you try to raise your PHP 4.3.10's error_reporting and enable display_errors? Somehow there must be a parse error or something like it...

Regards,
Garvin
# Garvin Hicking (s9y Developer)
# Did I help you? Consider making me happy: http://wishes.garv.in/
# or use my PayPal account "paypal {at} supergarv (dot) de"
# My "other" hobby: http://flickr.garv.in/
serendipity-fan
Regular
Posts: 7
Joined: Fri Oct 03, 2008 11:02 am

Post by serendipity-fan »

Yep, it's an odd bug and a quite persistent one unfortunately. Could you point me to the line in Serendipity where plugins are actually loaded?

Cheers, Serendipity-Fan
garvinhicking
Core Developer
Posts: 30022
Joined: Tue Sep 16, 2003 9:45 pm
Location: Cologne, Germany
Contact:

Post by garvinhicking »

Hi!

What about raising the error_reporting?

The plugins are loaded in the include/plugin_api.inc.php file, check the
load_plugin method and places where it's used.

Also check include/admin/plugins.inc.php at around line 165, where "backend_plugins_fetchlist" can be found. The lines after this hook is the place where the plugin list is enumerated and iterated.

Regards,
Garvin
# Garvin Hicking (s9y Developer)
# Did I help you? Consider making me happy: http://wishes.garv.in/
# or use my PayPal account "paypal {at} supergarv (dot) de"
# My "other" hobby: http://flickr.garv.in/
serendipity-fan
Regular
Posts: 7
Joined: Fri Oct 03, 2008 11:02 am

Post by serendipity-fan »

Thanks Garvin,

Yes, that will probably be the next step... Error reporting is enabled, but quiet. PHP doesn't report any errors. I'll try it out on another installation before I go to modify the plugin_api.inc.php to insert some debug code. Possibly the installation is screwed up.

Appreciate your input!

Cheers, Serendipty-Fan
serendipity-fan
Regular
Posts: 7
Joined: Fri Oct 03, 2008 11:02 am

Post by serendipity-fan »

I hope you don't mind if I revive this thread. In the meantime, I have added some debug code into the load_plugin() function in the plugin_api.inc.php module to see what goes wrong. It turns out that the line

Code: Select all

include($pluginFile)
which is supposed to load the class is never executed.

The reason is that the variable $pluginFile is empty and thus the condition

Code: Select all

if (!class_exists($class_name) && !empty($pluginFile))
is never true. At this point in the code, the local variables have the following values:

Code: Select all

$instance_id = "serendipity_event_popular"
$authorid =
$pluginFile=
$pluginPath=serendipity_event_popular
It seems there's something odd about the code logic at this point, because the class file isn't even loaded. Any suggestions would be very much appreciated.

Cheers, Serendipty-Fan
garvinhicking
Core Developer
Posts: 30022
Joined: Tue Sep 16, 2003 9:45 pm
Location: Cologne, Germany
Contact:

Post by garvinhicking »

Hi!

Actually, it should be that $pluginFile is "null", because the serendipity API get_event_plugins() method does not supply a value, so the default from load_plugin() should kick in. This in turn should execute:

Code: Select all

       if ($pluginFile === null) {
            $class_name = '';
            // $serendipity['debug']['pluginload'][] = "Init probe for plugin $instance_id, $class_name, $pluginPath";
            $pluginFile = serendipity_plugin_api::probePlugin($instance_id, $class_name, $pluginPath);
And probePlugin() does more searching for the file. The only way I see it sets it to "false" if no file is found in the proper path::

Code: Select all

           if (empty($filename)) {
                $serendipity['debug']['pluginload'][] = "No valid path/filename found. Aborting.";
                $retval = false;
                return $retval;
            }
This previously executes the includePlugin() method which stitches together a full $file:

Code: Select all

      if (!empty($instance_id) && $instance_id[0] == '@') {
            $file = S9Y_INCLUDE_PATH . 'include/plugin_internal.inc.php';
        } elseif (file_exists($serendipity['serendipityPath'] . $pluginFile)) {
            $file = $serendipity['serendipityPath'] . $pluginFile;
        } elseif (file_exists(S9Y_INCLUDE_PATH . $pluginFile)) {
            $file = S9Y_INCLUDE_PATH . $pluginFile;
        }

Can you check to what $file is set here, and if the exact same path is valid and existS?

You might also want to check your serendipity_plugins DB table and see what the "path" column is set to for this plugin. It should either be empty, or "serendipity_plugin_popular"?

Also check your dir-structur -- maybe you missing that path?!

Regards,
Garvin
# Garvin Hicking (s9y Developer)
# Did I help you? Consider making me happy: http://wishes.garv.in/
# or use my PayPal account "paypal {at} supergarv (dot) de"
# My "other" hobby: http://flickr.garv.in/
serendipity-fan
Regular
Posts: 7
Joined: Fri Oct 03, 2008 11:02 am

Post by serendipity-fan »

Thank you, Garvin, that was very helpful.

The program does not run that branch, because the parameter $pluginFile is set to boolean FALSE, which means that ($pluginFile === null) evaluates to FALSE and empty($pluginFile) evaluates to TRUE. Hence, the program calls getClassByInstanceId() in the else branch. This leaves $pluginFile untouched and hence no file searching/probing is ever done.

Cheers, Serendipity-Fan
garvinhicking
Core Developer
Posts: 30022
Joined: Tue Sep 16, 2003 9:45 pm
Location: Cologne, Germany
Contact:

Post by garvinhicking »

Hi!

Yeah, but the question is, where is $pluginFile set to false? This is why I posted that code, so that you can track down where it is set to FALSE. It should be NULL at least.

Regards,
Garvin
# Garvin Hicking (s9y Developer)
# Did I help you? Consider making me happy: http://wishes.garv.in/
# or use my PayPal account "paypal {at} supergarv (dot) de"
# My "other" hobby: http://flickr.garv.in/
serendipity-fan
Regular
Posts: 7
Joined: Fri Oct 03, 2008 11:02 am

Post by serendipity-fan »

garvinhicking wrote:Yeah, but the question is, where is $pluginFile set to false?
Right, that's exactly the question. I found that the includePlugin() function makes a number of calls to file_exists() which return false although they should to return true, because the plugin file does exist. The reason these calls return false is that the server has PHP SafeMode enabled and the plugin files were copied manually which means they have a different UID/GID on the Unix system. So the problem lies neither with the plugin code nor with Serendipity, but with an inappropriate server configuration. Case solved. That was a tough one. :roll: Thanks for pointing me into the right direction. I will suggest to my boss to release the plugin code as open source, so that the community can use it. Should be no problem, since posting the code here was already okayed.

Cheers, Serendipity-Fan
garvinhicking
Core Developer
Posts: 30022
Joined: Tue Sep 16, 2003 9:45 pm
Location: Cologne, Germany
Contact:

Post by garvinhicking »

Hi!

Phew. Hard one to catch :)

Best regards,
Garvin
# Garvin Hicking (s9y Developer)
# Did I help you? Consider making me happy: http://wishes.garv.in/
# or use my PayPal account "paypal {at} supergarv (dot) de"
# My "other" hobby: http://flickr.garv.in/
Post Reply