Index: plugins/serendipity_event_bbcode/serendipity_event_bbcode.php
===================================================================
--- plugins/serendipity_event_bbcode/serendipity_event_bbcode.php	(revision 210)
+++ plugins/serendipity_event_bbcode/serendipity_event_bbcode.php	(working copy)
@@ -40,6 +40,7 @@
         ));
         $propbag->add('cachable_events', array('frontend_display' => true));
         $propbag->add('event_hooks',   array('frontend_display' => true, 'frontend_comment' => true, 'css' => true));
+        $propbag->add('groups', array('MARKUP'));
 
         $this->markup_elements = array(
             array(
Index: plugins/serendipity_event_karma/serendipity_event_karma.php
===================================================================
--- plugins/serendipity_event_karma/serendipity_event_karma.php	(revision 210)
+++ plugins/serendipity_event_karma/serendipity_event_karma.php	(working copy)
@@ -123,7 +123,7 @@
         ));
         $propbag->add('event_hooks',   array('frontend_configure' => true, 'entry_display' => true, 'css' => true, 'event_additional_statistics' => true));
         $propbag->add('scrambles_true_content', true);
-
+        $propbag->add('groups', array('STATISTICS', 'FRONTEND_ENTRY'));
         $propbag->add('configuration', array('karma_active', 'visits_active', 'max_entrytime', 'max_votetime', 'extended_only', 'max_karmatime', 'logging'));
     }
 
Index: plugins/serendipity_event_s9ymarkup/serendipity_event_s9ymarkup.php
===================================================================
--- plugins/serendipity_event_s9ymarkup/serendipity_event_s9ymarkup.php	(revision 210)
+++ plugins/serendipity_event_s9ymarkup/serendipity_event_s9ymarkup.php	(working copy)
@@ -41,6 +41,7 @@
         ));
         $propbag->add('cachable_events', array('frontend_display' => true));
         $propbag->add('event_hooks',   array('frontend_display' => true, 'frontend_comment' => true));
+        $propbag->add('groups', array('MARKUP'));
 
         $this->markup_elements = array(
             array(
Index: plugins/serendipity_event_spartacus/serendipity_event_spartacus.php
===================================================================
--- plugins/serendipity_event_spartacus/serendipity_event_spartacus.php	(revision 210)
+++ plugins/serendipity_event_spartacus/serendipity_event_spartacus.php	(working copy)
@@ -24,6 +24,9 @@
         @define('PLUGIN_EVENT_SPARTACUS_FETCHED_BYTES_URL', '%s bytes von obiger URL geladen. Speichere Inhalt als %s...');
         @define('PLUGIN_EVENT_SPARTACUS_FETCHED_BYTES_CACHE', '%s bytes von bereits bestehender Datei geladen. Speichere Inhalt als %s...');
         @define('PLUGIN_EVENT_SPARTACUS_FETCHED_DONE', 'Daten erfolgreich geladen.');
+        @define('PLUGIN_EVENT_SPARTACUS_MIRROR_XML', 'Datei/Mirror Speicherort (XML-Metadaten)');
+        @define('PLUGIN_EVENT_SPARTACUS_MIRROR_FILES', 'Datei/Mirror Speicherort (Downloads)');
+        @define('PLUGIN_EVENT_SPARTACUS_MIRROR_DESC', 'Wählen Sie den Download-Server. Ändern Sie diesen Wert nur, wenn Sie wissen, was Sie tun und ein Server nicht mehr reagiert. Diese Option ist hauptsächlich für zukünftige Server reserviert.');
         break;
 
     case 'en':
@@ -36,13 +39,22 @@
         @define('PLUGIN_EVENT_SPARTACUS_FETCHED_BYTES_URL', 'Fetched %s bytes from the URL above. Saving file as %s...');
         @define('PLUGIN_EVENT_SPARTACUS_FETCHED_BYTES_CACHE', 'Fetched %s bytes from already existing file on your server. Saving file as %s...');
         @define('PLUGIN_EVENT_SPARTACUS_FETCHED_DONE', 'Data successfully fetched.');
+        @define('PLUGIN_EVENT_SPARTACUS_MIRROR_XML', 'File/Mirror location (XML metadata)');
+        @define('PLUGIN_EVENT_SPARTACUS_MIRROR_FILES', 'File/Mirror location (files)');
+        @define('PLUGIN_EVENT_SPARTACUS_MIRROR_DESC', 'Choose a download location. Do NOT change this value unless you know what you are doing and if servers get oudated. This option is available mainly for forward compatibility.');
         break;
 }
 
 class serendipity_event_spartacus extends serendipity_event
 {
     var $title = PLUGIN_EVENT_SPARTACUS_NAME;
+    var $purgeCache = false;
 
+    function microtime_float() {
+        list($usec, $sec) = explode(" ", microtime());
+        return ((float)$usec + (float)$sec);
+    }
+
     function introspect(&$propbag)
     {
         global $serendipity;
@@ -51,25 +63,103 @@
         $propbag->add('description',   PLUGIN_EVENT_SPARTACUS_DESC);
         $propbag->add('stackable',     false);
         $propbag->add('author',        'Garvin Hicking');
-        $propbag->add('version',       '1.2');
+        $propbag->add('version',       '2.0');
         $propbag->add('requirements',  array(
-            'serendipity' => '0.8',
+            'serendipity' => '0.9',
             'smarty'      => '2.6.7',
             'php'         => '4.1.0'
         ));
         $propbag->add('event_hooks',    array(
-            'backend_plugins_sidebar_header' => true,
-            'backend_plugins_event_header'   => true,
+            'backend_plugins_fetchlist'          => true,
+            'backend_plugins_fetchplugin'        => true,
 
-            'backend_plugins_fetchlist'      => true,
-            'backend_plugins_fetchplugin'    => true
+            'backend_templates_fetchlist'        => true,
+            'backend_templates_fetchtemplate'    => true
         ));
+        $propbag->add('groups', array('SYSTEM'));
+        $propbag->add('configuration', array('mirror_xml', 'mirror_files'));
     }
 
     function generate_content(&$title) {
         $title = $this->title;
     }
 
+    function cleanup() {
+        // Purge cached XML files.
+        $files = serendipity_traversePath($serendipity['serendipityPath'] . PATH_SMARTY_COMPILE, '', false, '/package_.+\.xml$/');
+    
+        if (!is_array($files)) {
+            return false;
+        }
+    
+        foreach ($files as $file) {
+            printf(DELETING_FILE . '<br />', $file['name']);
+            @unlink($serendipity['serendipityPath'] . PATH_SMARTY_COMPILE . '/' . $file['name']);
+        }
+    }
+
+    function &getMirrors($type = 'xml', $loc = false) {
+        static $mirror = array(
+            'xml' => array(
+                'Netmirror.org',
+                's9y.org'
+            ),
+            
+            'files' => array(
+                'SourceForge.net',
+                'Netmirror.org',
+                's9y.org',
+                'BerliOS.de'
+            )
+        );
+        
+        static $http = array(
+            'xml' => array(
+                'http://netmirror.org/mirror/serendipity/',
+                'http://s9y.org/mirror/'
+            ),
+            
+            'files' => array(
+                'http://cvs.sourceforge.net/viewcvs.py/*checkout*/php-blog/',
+                'http://netmirror.org/mirror/serendipity/',
+                'http://s9y.org/mirror/',
+                'http://svn.berlios.de/viewcvs/serendipity/'
+            )
+        );
+        
+        if ($loc) {
+            return $http[$type];
+        } else {
+            return $mirror[$type];
+        }
+    }
+
+    function introspect_config_item($name, &$propbag) {
+        global $serendipity;
+
+        switch($name) {
+            case 'mirror_xml':
+                $propbag->add('type',        'select');
+                $propbag->add('name',        PLUGIN_EVENT_SPARTACUS_MIRROR_XML);
+                $propbag->add('description', PLUGIN_EVENT_SPARTACUS_MIRROR_DESC);
+                $propbag->add('select_values', $this->getMirrors('xml'));
+                $propbag->add('default',     0);
+                break;
+
+            case 'mirror_files':
+                $propbag->add('type',        'select');
+                $propbag->add('name',        PLUGIN_EVENT_SPARTACUS_MIRROR_FILES);
+                $propbag->add('description', PLUGIN_EVENT_SPARTACUS_MIRROR_DESC);
+                $propbag->add('select_values', $this->getMirrors('files'));
+                $propbag->add('default',     0);
+                break;
+
+            default:
+                return false;
+        }
+        return true;
+    }
+
     function GetChildren(&$vals, &$i) {
         $children = array();
         $cnt = sizeof($vals);
@@ -104,10 +194,10 @@
     }
 
     // Create recursive directories; begins at serendipity plugin root folder level
-    function rmkdir($dir) {
+    function rmkdir($dir, $sub = 'plugins') {
         global $serendipity;
 
-        $spaths = explode('/', $serendipity['serendipityPath'] . 'plugins/'); 
+        $spaths = explode('/', $serendipity['serendipityPath'] . $sub . '/'); 
         $paths  = explode('/', $dir);
         
         $stack  = ''; 
@@ -126,7 +216,7 @@
         return true;
     }
 
-    function &fetchfile($url, $target, $cacheTimeout = 0) {
+    function &fetchfile($url, $target, $cacheTimeout = 0, $decode_utf8 = false, $sub = 'plugins') {
         printf(PLUGIN_EVENT_SPARTACUS_FETCHING, '<a href="' . $url . '">' . basename($url) . '</a>');
         echo '<br />';
 
@@ -152,7 +242,7 @@
                 printf(PLUGIN_EVENT_SPARTACUS_FETCHED_BYTES_URL, strlen($data), $target);
                 echo '<br />';
                 $tdir = dirname($target);
-                if (!is_dir($tdir) && !$this->rmkdir($tdir)) {
+                if (!is_dir($tdir) && !$this->rmkdir($tdir, $sub)) {
                     printf(FILE_WRITE_ERROR, $tdir);
                     echo '<br />';
                     return false;
@@ -165,38 +255,90 @@
                     echo '<br />';
                     return false;
                 }
+                
+                if ($decode_utf8) {
+                    $this->decode($data);
+                }
 
                 fwrite($fp, $data);
                 fclose($fp);
                 echo PLUGIN_EVENT_SPARTACUS_FETCHED_DONE;                
                 echo '<br />';
+                $this->purgeCache = true;
             }
         }
 
         return $data;
     }
 
-    function &fetchOnline($type) {
+    function decode(&$data) {
+        switch (strtolower(LANG_CHARSET)) {
+            case 'utf-8':
+                // The XML file is UTF-8 format. No changes needed.
+                break;
+                
+            case 'iso-8859-1':
+                $data = utf8_decode($data);
+                break;
+                
+            default:
+                if (function_exists('iconv')) {
+                    $data = iconv('UTF-8', LANG_CHARSET, $data);
+                } elseif (function_exists('recode')) {
+                    $data = recode('utf-8..' . LANG_CHARSET, $data);
+                }
+                break;
+        }
+    }
+
+    function &fetchOnline($type, $no_cache = false) {
+        global $serendipity;
+
         switch($type) {
             // Sanitize to not fetch other URLs
             default:
             case 'event':
                 $url_type = 'event';
+                $i18n     = true;
                 break;
 
             case 'sidebar':
                 $url_type = 'sidebar';
+                $i18n     = true;
                 break;
+
+            case 'template':
+                $url_type = 'template';
+                $i18n     = false;
+                break;
         }
+        
+        if (!$i18n) {
+            $lang = '';
+        } elseif (isset($serendipity['languages'][$serendipity['lang']])) {
+            $lang = '_' . $serendipity['lang'];
+        } else {
+            $lang = '_en';
+        }
 
-        $url    = 'http://netmirror.org/mirror/serendipity/package_' . $url_type . '.xml';
+        $mirrors = $this->getMirrors('xml', true);
+        $mirror  = $mirrors[$this->get_config('mirror_xml', 0)];
+
+        $url    = $mirror . '/package_' . $url_type .  $lang . '.xml';
         $cacheTimeout = 60*60*12; // XML file is cached for half a day
-        $target = $serendipity['serendipityPath'] . 'templates_c/package_' . $url_type . '.xml';
+        $target = $serendipity['serendipityPath'] . PATH_SMARTY_COMPILE . '/package_' . $url_type . $lang . '.xml';
 
-        $xml = $this->fetchfile($url, $target, $cacheTimeout);
+        $xml = $this->fetchfile($url, $target, $cacheTimeout, true);
 
         echo '<br /><br />';
 
+        $new_crc  = md5($xml);
+        $last_crc = $this->get_config('last_crc_' . $url_type);
+        
+        if (!$no_cache && !$this->purgeCache && $last_crc == $new_crc) {
+            return 'cached';
+        }
+
         // XML functions
         $p = xml_parser_create();
         xml_parser_set_option($p, XML_OPTION_CASE_FOLDING, 0);
@@ -212,14 +354,80 @@
             'children'   => $this->GetChildren($vals, $i)
         );
 
+        $this->set_config('last_crc_' . $url_type, $new_crc);
+
         return $tree;
     }
 
+    function &getCachedPlugins(&$plugins, $type) {
+        global $serendipity;
+        static $pluginlist = null;
+
+        if ($pluginlist === null) {
+            $data = serendipity_db_query("SELECT p.*,
+                                                 pc.category
+                                            FROM {$serendipity['dbPrefix']}pluginlist AS p
+                                 LEFT OUTER JOIN {$serendipity['dbPrefix']}plugincategories AS pc
+                                              ON pc.class_name = p.class_name
+                                           WHERE p.pluginlocation = 'Spartacus' AND
+                                                 p.plugintype     = '" . serendipity_db_escape_string($type) . "'");
+
+            if (is_array($data)) {
+                foreach($data AS $p) {
+                    $p['stackable']    = serendipity_db_bool($p['stackable']);
+                    $p['requirements'] = unserialize($p['requirements']);
+                    $this->checkPlugin($p, $plugins, $p['pluginlocation']);
+
+                    if (!isset($pluginlist[$p['plugin_file']])) {
+                        $pluginlist[$p['plugin_file']] = $p;
+                    }
+
+                    $pluginlist[$p['plugin_file']]['groups'][] = $p['category'];
+                }
+            }
+        }
+        
+        return $pluginlist;
+    }
+
+    function checkPlugin(&$data, &$plugins, $type) {
+        $installable = true;
+        $upgradeLink = '';
+        if (in_array($data['class_name'], $plugins)) {
+            $infoplugin =& serendipity_plugin_api::load_plugin($data['class_name']);
+            if (is_object($infoplugin)) {
+                $bag    = new serendipity_property_bag;
+                $infoplugin->introspect($bag);
+                if ($bag->get('version') == $data['version']) {
+                    $installable = false;
+                } elseif (version_compare($bag->get('version'), $data['version'], '<')) {
+                    $data['upgradable']      = true;
+                    $data['upgrade_version'] = $pluginstack[$i]['version'];
+                    $data['version']         = $bag->get('version');
+                    $upgradeLink             = '&amp;serendipity[spartacus_upgrade]=true';
+                }
+            }
+        }
+
+        $data['installable']     = $installable;
+        $data['pluginPath']      = 'online_repository';
+        $data['pluginlocation']  = 'Spartacus';
+        $data['plugintype']      = $type;
+        $data['customURI']       = '&amp;serendipity[spartacus_fetch]=' . $type . $upgradeLink;
+
+        return true;
+    }
+    
     function &buildList(&$tree, $type) {
         $plugins = serendipity_plugin_api::get_installed_plugins();
+
+        if ($tree === 'cached') {
+            return $this->getCachedPlugins($plugins, $type);
+        }
+
         $pluginstack = array();
         $i = 0;
-
+        
         foreach($tree[0]['children'] AS $idx => $subtree) {
             if ($subtree['tag'] == 'package') {
                 $i++;
@@ -227,16 +435,22 @@
                 foreach($subtree['children'] AS $child => $childtree) {
                     switch($childtree['tag']) {
                         case 'name':
-                            $pluginstack[$i]['plugin_class'] = $childtree['value'];
-                            $pluginstack[$i]['class_name']   = $childtree['value'];
+                            $pluginstack[$i]['plugin_class']    = 
+                                $pluginstack[$i]['plugin_file'] = 
+                                $pluginstack[$i]['class_name']  = 
+                                $pluginstack[$i]['true_name']   = $childtree['value'];
                             break;
 
                         case 'summary':
                             $pluginstack[$i]['name']         = $childtree['value'];
                             break;
 
+                        case 'groups':
+                            $pluginstack[$i]['groups']       = explode(',', $childtree['value']);
+                            break;
+
                         case 'description':
-                            $pluginstack[$i]['desc']         = $childtree['value'];
+                            $pluginstack[$i]['description']  = $childtree['value'];
                             break;
 
                         case 'release':
@@ -260,38 +474,107 @@
                             break;
                     }
                 }
+                
+                $this->checkPlugin($pluginstack[$i], $plugins, $type);
 
-                $installable = true;
-                $upgradeLink = '';
-                if (in_array($pluginstack[$i]['class_name'], $plugins)) {
-                    $infoplugin =& serendipity_plugin_api::load_plugin($pluginstack[$i]['class_name']);
-                    if (is_object($infoplugin)) {
-                        $bag    = new serendipity_property_bag;
-                        $infoplugin->introspect($bag);
-                        if ($bag->get('version') == $pluginstack[$i]['version']) {
-                            $installable = false;
-                        } elseif (version_compare($bag->get('version'), $pluginstack[$i]['version'], '<')) {
-                            $pluginstack[$i]['upgradable']      = true;
-                            $pluginstack[$i]['upgrade_version'] = $pluginstack[$i]['version'];
-                            $pluginstack[$i]['version']         = $bag->get('version');
-                            $upgradeLink                        = '&amp;serendipity[spartacus_upgrade]=true';
-                        }
+                serendipity_plugin_api::setPluginInfo($pluginstack[$i], $pluginstack[$i]['plugin_file'], $i, $i, 'Spartacus');
+                // Remove the temporary $i reference, as the array should be associative
+                $plugname = $pluginstack[$i]['true_name'];
+                $pluginstack[$plugname] = $pluginstack[$i];
+                unset($pluginstack[$i]);
+            }
+        }
+
+        return $pluginstack;
+    }
+
+    function &buildTemplateList(&$tree) {
+        $pluginstack = array();
+        $i = 0;
+        
+        $mirrors = $this->getMirrors('files', true);
+        $mirror  = $mirrors[$this->get_config('mirror_files', 0)];
+
+        foreach($tree[0]['children'] AS $idx => $subtree) {
+            if ($subtree['tag'] == 'package') {
+                $i++;
+
+                foreach($subtree['children'] AS $child => $childtree) {
+                    switch($childtree['tag']) {
+                        case 'name':
+                            $pluginstack[$i]['name']         = $childtree['value'];
+                            break;
+
+                        case 'summary':
+                            $pluginstack[$i]['summary']      = $childtree['value'];
+                            break;
+
+                        case 'template':
+                            $pluginstack[$i]['template']  = $childtree['value'];
+                            break;
+
+                        case 'description':
+                            $pluginstack[$i]['description']  = $childtree['value'];
+                            break;
+
+                        case 'release':
+                            $pluginstack[$i]['version']      = $childtree['children'][0]['value'];
+                            
+                            $pluginstack[$i]['requirements'] = array(
+                                'serendipity' => '',
+                                'php'         => '',
+                                'smarty'      => ''
+                            );
+
+                            foreach($childtree['children'] AS $relInfo) {
+                                if ($relInfo['tag'] == 'requirements:s9yVersion') {
+                                    $pluginstack[$i]['requirements']['serendipity'] = $relInfo['value'];
+                                }
+
+                                if ($relInfo['tag'] == 'date') {
+                                    $pluginstack[$i]['date'] = $relInfo['value'];
+                                }
+
+                            }
+                            
+                            $pluginstack[$i]['require serendipity'] = $pluginstack[$i]['requirements']['serendipity'];
+                            break;
+
+                        case 'maintainers':
+                            $pluginstack[$i]['author']       = $childtree['children'][0]['children'][0]['value']; // I dig my PHP arrays ;-)
+                            break;
                     }
                 }
 
-                $pluginstack[$i]['installable'] = $installable;
-                $pluginstack[$i]['pluginPath']  = 'online_repository';
-                $pluginstack[$i]['customURI']   = '&amp;serendipity[spartacus_fetch]=' . $type . $upgradeLink;
+                $plugname = $pluginstack[$i]['template'];
+                $pluginstack[$i]['previewURL'] = $mirror . '/additional_themes/' . $plugname . '/preview.png?rev=1.9999';
+                $pluginstack[$i]['customURI']  = '&amp;serendipity[spartacus_fetch]=' . $plugname;
+                $pluginstack[$i]['customIcon'] = '_spartacus';
+
+                // Remove the temporary $i reference, as the array should be associative
+                $pluginstack[$plugname] = $pluginstack[$i];
+                unset($pluginstack[$i]);
             }
         }
 
         return $pluginstack;
     }
 
-    function download(&$tree, $plugin_to_install) {
+    function download(&$tree, $plugin_to_install, $sub = 'plugins') {
         global $serendipity;
 
-        $pdir = $serendipity['serendipityPath'] . '/plugins/';
+        switch($sub) {
+            case 'plugins':
+            default:
+                $sfloc = 'additional_plugins';
+                break;
+            
+            case 'templates':
+                $sfloc = 'additional_themes';
+                break;
+        }
+
+        $pdir = $serendipity['serendipityPath'] . '/' . $sub . '/';
         if (!is_writable($pdir)) {
             printf(DIRECTORY_WRITE_ERROR, $pdir);
             echo '<br />';
@@ -306,7 +589,7 @@
             }
 
             foreach($subtree['children'] AS $child => $childtree) {
-                if ($childtree['tag'] == 'name' && $childtree['value'] == $plugin_to_install) {
+                if ($childtree['tag'] == 'template' && $childtree['value'] == $plugin_to_install) {
                     $found = true;
                 }
 
@@ -330,8 +613,11 @@
             }
         }
 
+        $mirrors = $this->getMirrors('files', true);
+        $mirror  = $mirrors[$this->get_config('mirror_files', 0)];
+
         foreach($files AS $file) {
-            $url    = 'http://cvs.sourceforge.net/viewcvs.py/*checkout*/php-blog/additional_plugins/' . $file . '?rev=1.9999';
+            $url    = $mirror . '/' . $sfloc . '/' . $file . '?rev=1.9999';
             $target = $pdir . $file;
             @mkdir($pdir . $plugin_to_install);
             $this->fetchfile($url, $target);
@@ -353,30 +639,30 @@
 
         if (isset($hooks[$event])) {
             switch($event) {
-                case 'backend_plugins_sidebar_header':
-?>
-    <br /><a href="?serendipity[adminModule]=plugins&amp;serendipity[adminAction]=addnew&amp;serendipity[source]=spartacus" class="serendipityIconLink"><img src="<?php echo serendipity_getTemplateFile('admin/img/install.png') ?>" style="border: 0px none ; vertical-align: middle; display: inline;" alt=""><?php echo sprintf(PLUGIN_EVENT_SPARTACUS_FETCH, SIDEBAR_PLUGIN) ?></a>
-<?php
-                    return true;
-                    break;
+                case 'backend_templates_fetchlist':
+                    $eventData = $this->buildTemplateList($this->fetchOnline('template', true), 'template');
 
-                case 'backend_plugins_event_header':
-?>
-    <br /><a href="?serendipity[adminModule]=plugins&amp;serendipity[adminAction]=addnew&amp;serendipity[type]=event&amp;serendipity[source]=spartacus" class="serendipityIconLink"><img src="<?php echo serendipity_getTemplateFile('admin/img/install.png') ?>" style="border: 0px none ; vertical-align: middle; display: inline;" alt=""><?php echo sprintf(PLUGIN_EVENT_SPARTACUS_FETCH, EVENT_PLUGIN) ?></a>
-<?php
                     return true;
                     break;
 
-                case 'backend_plugins_fetchlist':
-                    if ($serendipity['GET']['source'] != 'spartacus') {
-                        return false;
+                case 'backend_templates_fetchtemplate':
+                    if (!empty($eventData['GET']['spartacus_fetch'])) {
+                        $this->download(
+                            $this->fetchOnline('template', true), 
+                            $eventData['GET']['theme'],
+                            'templates'
+                        );
                     }
 
+                    return false;
+                    break;
+                    
+                case 'backend_plugins_fetchlist':
                     $type = (isset($serendipity['GET']['type']) ? $serendipity['GET']['type'] : 'sidebar');
 
                     $eventData = array(
-                                   'pluginstack' => $this->buildList($this->fetchOnline($type), $type),
-                                   'errorstack'  => array()
+                       'pluginstack' => $this->buildList($this->fetchOnline($type), $type),
+                       'errorstack'  => array()
                     );
 
                     return true;
@@ -384,7 +670,7 @@
 
                 case 'backend_plugins_fetchplugin':
                     if (!empty($eventData['GET']['spartacus_fetch'])) {
-                        $baseDir = $this->download($this->fetchOnline($eventData['GET']['spartacus_fetch']), $eventData['GET']['install_plugin']);
+                        $baseDir = $this->download($this->fetchOnline($eventData['GET']['spartacus_fetch'], true), $eventData['GET']['install_plugin']);
                         if (!empty($baseDir)) {
                             $eventData['GET']['pluginPath'] = $baseDir;
                         } else {
@@ -410,4 +696,4 @@
 }
 
 /* vim: set sts=4 ts=4 expandtab : */
-?>
+?>
\ No newline at end of file
Index: plugins/serendipity_event_nl2br/serendipity_event_nl2br.php
===================================================================
--- plugins/serendipity_event_nl2br/serendipity_event_nl2br.php	(revision 210)
+++ plugins/serendipity_event_nl2br/serendipity_event_nl2br.php	(working copy)
@@ -39,6 +39,7 @@
         ));
         $propbag->add('cachable_events', array('frontend_display' => true));
         $propbag->add('event_hooks',   array('frontend_display' => true));
+        $propbag->add('groups', array('MARKUP'));
 
         $this->markup_elements = array(
             array(
Index: serendipity_config.inc.php
===================================================================
--- serendipity_config.inc.php	(revision 210)
+++ serendipity_config.inc.php	(working copy)
@@ -21,7 +21,7 @@
 include_once(S9Y_INCLUDE_PATH . 'include/compat.inc.php');
 
 // The version string
-$serendipity['version']         = '0.9-alpha3';
+$serendipity['version']         = '0.9-alpha4';
 
 // Name of folder for the default theme
 $serendipity['defaultTemplate'] = 'default';
Index: include/plugin_api.inc.php
===================================================================
--- include/plugin_api.inc.php	(revision 210)
+++ include/plugin_api.inc.php	(working copy)
@@ -152,9 +152,13 @@
 
             if ($p == 'serendipity_plugin' && $class_name != 'serendipity_event' && (!$event_only || is_null($event_only))) {
                 $classes[$class_name] = array('name'       => '@' . $class_name,
+                                              'type'       => 'internal_event',
+                                              'true_name'  => $class_name,
                                               'pluginPath' => '');
             } elseif ($p == 'serendipity_event' && $class_name != 'serendipity_event' && ($event_only || is_null($event_only))) {
                 $classes[$class_name] = array('name'       => '@' . $class_name,
+                                              'type'       => 'internal_plugin',
+                                              'true_name'  => $class_name,
                                               'pluginPath' => '');
             }
         }
@@ -179,35 +183,40 @@
                 if ($f{0} == '.' || $f == 'CVS' || !is_dir($ppath . '/' . $f)) {
                     continue;
                 }
-
+                
                 $subd = opendir($ppath . '/' . $f);
                 if (!$subd) {
                     continue;
                 }
 
                 // Instead of only looking for directories, search for files within subdirectories
+                $final_loop = false;
                 while (($subf = readdir($subd)) !== false) {
+                    
                     if ($subf{0} == '.' || $subf == 'CVS') {
                         continue;
                     }
 
-                    if (is_dir($ppath . '/' . $f . '/' . $subf) && $maindir != $ppath . '/' . $f) {
+                    if (!$final_loop && is_dir($ppath . '/' . $f . '/' . $subf) && $maindir != $ppath . '/' . $f) {
                         // Search for another level of subdirectories
                         serendipity_plugin_api::traverse_plugin_dir($ppath . '/' . $f, $classes, $event_only, $f . '/');
+                        // We can break after that operation because the current directory has been fully checked already.
+                        $final_loop = true;
                     }
 
                     if (!preg_match('@^[^_]+_(event|plugin)_.+\.php$@i', $subf)) {
                         continue;
                     }
 
+                    $class_name = str_replace('.php', '', $subf);
                     // If an external plugin/event already exists as internal, remove the internal reference because its redundant
-                    if (isset($classes['@' . $subf])) {
-                        unset($classes['@' . $subf]);
+                    if (isset($classes['@' . $class_name])) {
+                        unset($classes['@' . $class_name]);
                     }
 
                     // A local plugin will be preferred over general plugins [used when calling this function the second time]
-                    if (isset($classes[$subf])) {
-                        unset($classes[$subf]);
+                    if (isset($classes[$class_name])) {
+                        unset($classes[$class_name]);
                     }
 
                     if (!is_null($event_only) && $event_only && !serendipity_plugin_api::is_event_plugin($subf)) {
@@ -218,11 +227,14 @@
                         continue;
                     }
 
-                    $class_name = str_replace('.php', '', $subf);
                     $classes[$class_name] = array('name'       => $class_name,
+                                                  'true_name'  => $class_name,
+                                                  'type'       => 'additional_plugin',
                                                   'pluginPath' => $maindir . $f);
                 }
+                closedir($subd);
             }
+            closedir($d);
         }
     }
 
@@ -301,29 +313,45 @@
         if (empty($pluginPath)) {
             $pluginPath = $name;
         }
+        
+        $file = false;
 
         // First try the local path, and then (if existing) a shared library repository ...
         if (file_exists($serendipity['serendipityPath'] . 'plugins/' . $pluginPath . '/' . $name . '.php')) {
-            include_once $serendipity['serendipityPath'] . 'plugins/' . $pluginPath . '/' . $name . '.php';
+            $file = $serendipity['serendipityPath'] . 'plugins/' . $pluginPath . '/' . $name . '.php';
         } elseif (file_exists(S9Y_INCLUDE_PATH . 'plugins/' . $pluginPath . '/' . $name . '.php')) {
-            include_once S9Y_INCLUDE_PATH . 'plugins/' . $pluginPath . '/' . $name . '.php';
+            $file = S9Y_INCLUDE_PATH . 'plugins/' . $pluginPath . '/' . $name . '.php';
         }
+        
+        return $file;
     }
 
-    /* Creates an instance of a named plugin */
-    function &load_plugin($instance_id, $authorid = null, $pluginPath = '')
-    {
-        global $serendipity;
-
+    function getClassByInstanceID($instance_id, &$is_internal) {
         $instance = explode(':', $instance_id);
         $name     = $instance[0];
 
         if ($name{0} == '@') {
             $class_name = substr($name, 1);
         } else {
+            $class_name =& $name;
+        }
+        
+        return $class_name;
+    }
+    
+    /* Probes for the plugin filename */
+    function probePlugin($instance_id, &$class_name, &$pluginPath) {
+        global $serendipity;
+
+        $filename    = false;
+        $is_internal = false;
+        
+        $class_name  = serendipity_plugin_api::getClassByInstanceID($instance_id, $is_internal);
+
+        if (!$is_internal) {
             /* plugin from the plugins/ dir */
-            serendipity_plugin_api::includePlugin($name, $pluginPath);
-            if (!class_exists($name) && empty($pluginPath)) {
+            $filename = serendipity_plugin_api::includePlugin($class_name, $pluginPath);
+            if (empty($filename) && !empty($instance_id)) {
                 $sql = "SELECT path from {$serendipity['dbPrefix']}plugins WHERE name = '" . $instance_id . "'";
                 $plugdata = serendipity_db_query($sql, true, 'both', false, false, false, true);
                 if (is_array($plugdata) && isset($plugdata[0])) {
@@ -331,19 +359,36 @@
                 }
 
                 if (empty($pluginPath)) {
-                    $pluginPath = $name;
+                    $pluginPath = $class_name;
                 }
         
-                serendipity_plugin_api::includePlugin($name, $pluginPath);
+                $filename = serendipity_plugin_api::includePlugin($class_name, $pluginPath);
             }
 
-            if (!class_exists($name)) {
+            if (empty($filename)) {
                 return false;
             }
+        }
 
-            $class_name =& $name;
+        return $filename;
+    }
+    
+    /* Creates an instance of a named plugin */
+    function &load_plugin($instance_id, $authorid = null, $pluginPath = '', $pluginFile = null) {
+        global $serendipity;
+
+        if ($pluginFile === null) {
+            $class_name = '';
+            $pluginFile = serendipity_plugin_api::probePlugin($instance_id, $class_name, $pluginPath);
+        } else {
+            $is_internal = false;
+            $class_name  = serendipity_plugin_api::getClassByInstanceID($instance_id, $is_internal);
         }
-
+        
+        if (!class_exists($class_name) && (empty($pluginFile) || !include_once($pluginFile))) {
+            return false;
+        }
+        
         $p =& new $class_name($instance_id);
         if (!is_null($authorid)) {
             $p->serendipity_owner = $authorid;
@@ -358,6 +403,98 @@
         return $p;
     }
 
+    function &getPluginInfo(&$pluginFile, &$class_data, $type) {
+        global $serendipity;
+
+        static $pluginlist = null;
+        
+        if ($pluginlist === null) {
+            $data = serendipity_db_query("SELECT p.*,
+                                                 pc.category
+                                            FROM {$serendipity['dbPrefix']}pluginlist AS p
+                                 LEFT OUTER JOIN {$serendipity['dbPrefix']}plugincategories AS pc
+                                              ON pc.class_name = p.class_name
+                                           WHERE p.pluginlocation = 'local' AND
+                                                 p.plugintype     = '" . serendipity_db_escape_string($type) . "'");
+            if (is_array($data)) {
+                foreach($data AS $p) {
+                    if (!isset($pluginlist[$p['plugin_file']])) {
+                        $pluginlist[$p['plugin_file']] = $p;
+                    }
+
+                    $pluginlist[$p['plugin_file']]['groups'][] = $p['category'];
+                }
+            }
+        }
+        
+        if (is_array($pluginlist[$pluginFile])) {
+            $data = $pluginlist[$pluginFile];
+            if ((int)filemtime($pluginFile) == (int)$data['last_modified']) {
+                $data['stackable']    = serendipity_db_bool($data['stackable']);
+
+                $plugin    = $data;
+                return $plugin;
+            }
+        }
+        
+        $plugin =& serendipity_plugin_api::load_plugin($class_data['name'], null, $class_data['pluginPath'], $pluginFile);
+        
+        return $plugin;
+    }
+    
+    function &setPluginInfo(&$plugin, &$pluginFile, &$bag, &$class_data, $pluginlocation = 'local') {
+        global $serendipity;
+
+        serendipity_db_query("DELETE FROM {$serendipity['dbPrefix']}pluginlist WHERE plugin_file = '" . serendipity_db_escape_string($pluginFile) . "' AND pluginlocation = '" . serendipity_db_escape_string($pluginlocation) . "'");
+        
+        if (is_object($plugin)) {
+            $data = array(
+                'class_name'      => get_class($plugin),
+                'stackable'       => $bag->get('stackable'),
+                'name'            => $bag->get('name'),
+                'description'     => $bag->get('description'),
+                'author'          => $bag->get('author'),
+                'version'         => $bag->get('version'),
+                'upgrade_version' => $bag->get('version'),
+                'requirements'    => serialize($bag->get('requirements')),
+                'website'         => $bag->get('website'),
+                'plugin_class'    => $class_data['name'],
+                'pluginPath'      => $class_data['pluginPath'],
+                'plugin_file'     => $pluginFile,
+                'pluginlocation'  => $pluginlocation,
+                'plugintype'      => $serendipity['GET']['type'],
+                'last_modified'   => filemtime($pluginFile)
+            );
+            $groups = $bag->get('groups');
+        } elseif (is_array($plugin)) {
+            $data = $plugin;
+            $groups = $data['groups'];
+            unset($data['installable']);
+            unset($data['true_name']);
+            unset($data['customURI']);
+            unset($data['groups']);
+            $data['requirements'] = serialize($data['requirements']);
+        }
+
+        serendipity_db_insert('pluginlist', $data);
+
+        serendipity_db_query("DELETE FROM {$serendipity['dbPrefix']}plugincategories WHERE class_name = '" . serendipity_db_escape_string($data['class_name']) . "'");
+        foreach((array)$groups AS $group) {
+            if (empty($group)) {
+                continue;
+            }
+
+            $cat = array(
+                'class_name'  => $data['class_name'],
+                'category'    => $group
+            );
+        }
+        
+        $data['groups'] = $groups;
+
+        return $data;
+    }
+
     function update_plugin_placement($name, $placement, $order=null)
     {
         global $serendipity;
Index: include/admin/plugins.inc.php
===================================================================
--- include/admin/plugins.inc.php	(revision 210)
+++ include/admin/plugins.inc.php	(working copy)
@@ -13,8 +13,16 @@
 include_once S9Y_INCLUDE_PATH . 'include/plugin_api.inc.php';
 include_once S9Y_INCLUDE_PATH . 'include/plugin_internal.inc.php';
 
+function serendipity_groupname($group) {
+    if (defined('PLUGIN_GROUP_' . $group)) {
+        return constant('PLUGIN_GROUP_' . $group);
+    } else {
+        return $group;
+    }
+}
+
 function serendipity_pluginListSort($x, $y) {
-    return strnatcasecmp($x['name'] . ' - ' . $x['desc'], $y['name'] . ' - ' . $y['desc']);
+    return strnatcasecmp($x['name'] . ' - ' . $x['description'], $y['name'] . ' - ' . $y['description']);
 }
 
 function show_plugins($event_only = false)
@@ -348,7 +356,7 @@
                 foreach($select AS $select_value => $select_desc) {
                     $id = htmlspecialchars($config_item . $select_value);
 ?>
-                        <option value="<?php echo $select_value; ?>" <?php echo ($select_value == $hvalue ? 'selected="selected"' : ''); ?> title="<?php echo htmlspecialchars($select_desc); ?>" />
+                        <option value="<?php echo $select_value; ?>" <?php echo ($select_value == $hvalue ? 'selected="selected"' : ''); ?> title="<?php echo htmlspecialchars($select_desc); ?>">
                             <?php echo htmlspecialchars($select_desc); ?>
                         </option>
 <?php
@@ -506,133 +514,147 @@
 <?php } else { ?>
     <h2><?php echo SIDEBAR_PLUGINS ?></h2>
 <?php } ?>
-<?php echo SELECT_A_PLUGIN_TO_ADD; ?>
 <br />
-<br />
 <?php
-    $foreignPlugins = array();
+    $foreignPlugins = $pluginstack = $errorstack = array();
     serendipity_plugin_api::hook_event('backend_plugins_fetchlist', $foreignPlugins);
-?>
-<table cellspacing="0" cellpadding="0" border="0" width="100%">
-    <tr>
-        <td><strong>Plugin</strong></td>
-        <td width="100" align="center"><strong>Action</strong></td>
-    </tr>
-<?php
-    $plugin_navigation = '';
+    $pluginstack = array_merge($foreignPlugins['pluginstack'], $pluginstack);
+    $errorstack  = array_merge($foreignPlugins['errorstack'], $errorstack);
 
-    if (count($foreignPlugins) > 0) {
-        $pluginstack = $foreignPlugins['pluginstack'];
-        $errorstack  = $foreignPlugins['errorstack'];
-    } else {
-        $pluginsPerPage = 45;
-        $currentPage    = (int)(!empty($serendipity['GET']['page']) ? $serendipity['GET']['page'] : 1);
-        $countStart     = ($currentPage-1) * $pluginsPerPage;
-        $countEnd       = ($currentPage)   * $pluginsPerPage; 
+    $plugins = serendipity_plugin_api::get_installed_plugins();
+    $classes = serendipity_plugin_api::enum_plugin_classes(($serendipity['GET']['type'] == 'event'));
+    usort($classes, 'serendipity_pluginListSort'); 
 
-        $plugins = serendipity_plugin_api::get_installed_plugins();
+    $counter    = 0;
+    foreach ($classes as $class_data) {
 
-        $errorstack = $pluginstack = array();
-        $classes    = serendipity_plugin_api::enum_plugin_classes(($serendipity['GET']['type'] == 'event'));
-        // TODO: With pagination of the plugins we cannot get the localized title of the plugins before sorting it.
-        //       This means we get a weird order of the plugins. We need to find a way
-        //       to do the sorting on the real plugin name WITHOUT loading ALL dozen plugins
-        usort($classes, 'serendipity_pluginListSort'); 
+        $pluginFile =  serendipity_plugin_api::probePlugin($class_data['name'], $class_data['classname'], $class_data['pluginPath']);
+        $plugin     =& serendipity_plugin_api::getPluginInfo($pluginFile, $class_data, $serendipity['GET']['type']);
 
-        $maxPage           = ceil(count($classes)/$pluginsPerPage);
-        $plugin_navigation = '<tr><td colspan="2" align="center">' . sprintf(PAGE_BROWSE_PLUGINS, $currentPage, $maxPage, count($classes)) . '<br />';
-        $plugin_pages      = array();
-        for ($i = 1; $i <= $maxPage; $i++) {
-            $plugin_page = '';
-            if ($i == $currentPage) {
-                $plugin_page .= '<strong>';
-            }
+        if (is_object($plugin)) {
+            // Object is returned when a plugin could not be cached.
+            $bag = new serendipity_property_bag;
+            $plugin->introspect($bag);
+            $props = serendipity_plugin_api::setPluginInfo($plugin, $pluginFile, $bag, $class_data);
+            $counter++;
+        } elseif (is_array($plugin)) {
+            // Array is returned if a plugin could be fetched from info cache
+            $props = $plugin;
+        } else {
+            $props = false;
+        }
+        
+        if (is_array($props)) {
+            $props['installable']  = !($props['stackable'] === false && in_array($class_data['true_name'], $plugins));
+            $props['requirements'] = unserialize($props['requirements']);
 
-            $plugin_page .= '<a href="?serendipity[adminModule]=plugins&amp;serendipity[adminAction]=addnew&amp;serendipity[type]=' . $serendipity['GET']['type'] . '&amp;serendipity[page]=' . $i . '">' . $i . '</a>';
-
-            if ($i == $currentPage) {
-                $plugin_page .= '</strong>';
-            }
-            
-            $plugin_pages[] = $plugin_page;
+            $pluginstack[$class_data['true_name']] = $props;
+        } else {
+            // False is returned if a plugin could not be instantiated
+            $errorstack[] = $class_data['true_name'];
         }
-        $plugin_navigation .= implode(' | ', $plugin_pages);
-      
-        echo $plugin_navigation;
+    }
 
-        $counter    = 0;
-        foreach ($classes as $class_data) {
-            $counter++;
-
-            if ($counter < $countStart || $counter > $countEnd) {
-                continue;
-            }
-
-            $plugin =& serendipity_plugin_api::load_plugin($class_data['name'], null, $class_data['pluginPath']);
-            if (is_object($plugin)) {
-                $bag = new serendipity_property_bag;
-                $plugin->introspect($bag);
-                $pluginstack[] = array('plugin_class'   => $class_data['name'],
-                                       'class_name'     => get_class($plugin),
-                                       'name'           => $bag->get('name'),
-                                       'desc'           => $bag->get('description'),
-                                       'installable'    => !($bag->get('stackable') === false && in_array($class_data['name'], $plugins)),
-                                       'author'         => $bag->get('author'),
-                                       'version'        => $bag->get('version'),
-                                       'requirements'   => $bag->get('requirements'),
-                                       'website'        => $bag->get('website'),
-                                       'pluginPath'     => $class_data['pluginPath']);
-            } else {
-                $errorstack[] = $class_data['name'];
-            }
+    usort($pluginstack, 'serendipity_pluginListSort');
+    $pluggroups = array();
+    foreach($pluginstack AS $plugname => $plugdata) {
+        foreach((array)$plugdata['groups'] AS $group) {
+            $pluggroups[$group][] = $plugdata;
         }
     }
 
-    usort($pluginstack, 'serendipity_pluginListSort');
     foreach($errorstack as $e_idx => $e_name) {
         echo ERROR . ': ' . $e_name . '<br />';
     }
-
-    foreach ($pluginstack as $plug) {
-        $jsLine = " onmouseout=\"document.getElementById('serendipity_plugin_". $plug['class_name'] ."').className='';\"";
-        $jsLine .= " onmouseover=\"document.getElementById('serendipity_plugin_". $plug['class_name'] ."').className='serendipity_admin_list_item_uneven';\"";
-
-        $pluginInfo = $notice = array();
-        if (!empty($plug['author'])) {
-            $pluginInfo[] = AUTHOR  . ': ' . $plug['author'];
+?>
+<table cellspacing="0" cellpadding="0" border="0" width="100%">
+<?php
+    $available_groups = array_keys($pluggroups);
+    foreach($pluggroups AS $pluggroup => $groupstack) {
+        if (empty($pluggroup)) {
+        ?>
+    <tr>
+        <td colspan="2" class="serendipity_pluginlist_header">
+            <form action="serendipity_admin.php" method="get">
+                <input type="hidden" name="serendipity[adminModule]" value="plugins" />
+                <input type="hidden" name="serendipity[adminAction]" value="addnew" />
+                <input type="hidden" name="serendipity[type]" value="<?php echo htmlspecialchars($serendipity['GET']['type']); ?>" />
+                <?php echo FILTERS; ?>: <select name="serendipity[only_group]">
+            <?php foreach((array)$available_groups AS $available_group) { 
+                    ?>
+                    <option value="<?php echo $available_group; ?>" <?php echo ($serendipity['GET']['only_group'] == $available_group ? 'selected="selected"' : ''); ?>><?php echo serendipity_groupname($available_group); ?>
+            <?php } ?>
+                </select>
+                <input type="submit" value="<?php echo GO; ?>" />
+            </form>
+        </td>
+    </tr>
+        <?php
+            if (!empty($serendipity['GET']['only_group'])) {
+                continue;
+            }
+        } elseif (!empty($serendipity['GET']['only_group']) && $pluggroup != $serendipity['GET']['only_group']) {
+            continue;
+        } else {
+        ?>
+    <tr>
+        <td colspan="2" class="serendipity_pluginlist_section"><strong><?php echo serendipity_groupname($pluggroup); ?></strong></td>
+    </tr>
+        <?php
         }
+?>
+    <tr>
+        <td><strong>Plugin</strong></td>
+        <td width="100" align="center"><strong>Action</strong></td>
+    </tr>
+<?php
+        foreach ($groupstack as $plug) {
+            $jsLine = " onmouseout=\"document.getElementById('serendipity_plugin_". $plug['class_name'] ."').className='';\"";
+            $jsLine .= " onmouseover=\"document.getElementById('serendipity_plugin_". $plug['class_name'] ."').className='serendipity_admin_list_item_uneven';\"";
+    
+            $pluginInfo = $notice = array();
+            if (!empty($plug['author'])) {
+                $pluginInfo[] = AUTHOR  . ': ' . $plug['author'];
+            }
+    
+            if (!empty($plug['version'])) {
+                $pluginInfo[] = VERSION  . ': ' . $plug['version'];
+            }
+    
+            if (!empty($plug['upgrade_version']) && $plug['upgrade_version'] != $plug['version']) {
+                $pluginInfo[] = sprintf(UPGRADE_TO_VERSION, $plug['upgrade_version']);
+            }
+            
+            if (!empty($plug['pluginlocation']) && $plug['pluginlocation'] != 'local') {
+                $pluginInfo[] = '(' . htmlspecialchars($plug['pluginlocation']) . ')';
+                $installimage = serendipity_getTemplateFile('admin/img/install_now_' . strtolower($plug['pluginlocation']) . '.png');
+            } else {
+                $installimage = serendipity_getTemplateFile('admin/img/install_now.png');
+            }
+    
+            if (!isset($plug['customURI'])) {
+                $plug['customURI'] = '';
+            }
+    
+            if ( !empty($plug['requirements']['serendipity']) && version_compare($plug['requirements']['serendipity'], serendipity_getCoreVersion($serendipity['version']), '>') ) {
+                $notice['requirements_failures'][] = 's9y ' . $plug['requirements']['serendipity'];
+            }
+    
+            if ( !empty($plug['requirements']['php']) && version_compare($plug['requirements']['php'], phpversion(), '>') ) {
+                $notice['requirements_failures'][] = 'PHP ' . $plug['requirements']['php'];
+            }
+    
+            /* Enable after Smarty 2.6.7 upgrade.
+             * TODO: How can we get current Smarty version here? $smarty is not created!
+            if ( !empty($plug['requirements']['smarty']) && version_compare($plug['requirements']['smarty'], '2.6.7', '>') ) {
+                $notice['requirements_failures'][] = 'Smarty: ' . $plug['requirements']['smarty'];
+            }
+            */
+    
+            if (count($notice['requirements_failures']) > 0) {
+                $plug['requirements_fail'] = true;
+            }
 
-        if (!empty($plug['version'])) {
-            $pluginInfo[] = VERSION  . ': ' . $plug['version'];
-        }
-
-        if (!empty($plug['upgrade_version'])) {
-            $pluginInfo[] = sprintf(UPGRADE_TO_VERSION, $plug['upgrade_version']);
-        }
-
-        if (!isset($plug['customURI'])) {
-            $plug['customURI'] = '';
-        }
-
-        if ( !empty($plug['requirements']['serendipity']) && version_compare($plug['requirements']['serendipity'], serendipity_getCoreVersion($serendipity['version']), '>') ) {
-            $notice['requirements_failures'][] = 's9y ' . $plug['requirements']['serendipity'];
-        }
-
-        if ( !empty($plug['requirements']['php']) && version_compare($plug['requirements']['php'], phpversion(), '>') ) {
-            $notice['requirements_failures'][] = 'PHP ' . $plug['requirements']['php'];
-        }
-
-        /* Enable after Smarty 2.6.7 upgrade.
-         * TODO: How can we get current Smarty version here? $smarty is not created!
-        if ( !empty($plug['requirements']['smarty']) && version_compare($plug['requirements']['smarty'], '2.6.7', '>') ) {
-            $notice['requirements_failures'][] = 'Smarty: ' . $plug['requirements']['smarty'];
-        }
-        */
-
-        if (count($notice['requirements_failures']) > 0) {
-            $plug['requirements_fail'] = true;
-        }
-
 ?>
     <tr id="serendipity_plugin_<?php echo $plug['class_name']; ?>">
         <td colspan="2" <?php echo $jsLine ?>>
@@ -645,27 +667,26 @@
                         <?php } elseif ( $plug['upgradable'] == true ) { ?>
                             <a href="?serendipity[adminModule]=plugins&amp;serendipity[pluginPath]=<?php echo $plug['pluginPath']; ?>&amp;serendipity[install_plugin]=<?php echo $plug['plugin_class'] . $plug['customURI'] ?>"><img src="<?php echo serendipity_getTemplateFile('admin/img/upgrade_now.png') ?>" title="<?php echo UPGRADE ?>" alt="<?php echo UPGRADE ?>" border="0"></a>
                         <?php } elseif ($plug['installable'] == true) { ?>
-                            <a href="?serendipity[adminModule]=plugins&amp;serendipity[pluginPath]=<?php echo $plug['pluginPath']; ?>&amp;serendipity[install_plugin]=<?php echo $plug['plugin_class'] . $plug['customURI'] ?>"><img src="<?php echo serendipity_getTemplateFile('admin/img/install_now.png') ?>" title="<?php echo INSTALL ?>" alt="<?php echo INSTALL ?>" border="0"></a>
+                            <a href="?serendipity[adminModule]=plugins&amp;serendipity[pluginPath]=<?php echo $plug['pluginPath']; ?>&amp;serendipity[install_plugin]=<?php echo $plug['plugin_class'] . $plug['customURI'] ?>"><img src="<?php echo $installimage ?>" title="<?php echo INSTALL ?>" alt="<?php echo INSTALL ?>" border="0"></a>
                         <?php } else { ?>
                             <span style="color: #cccccc"><?php echo ALREADY_INSTALLED ?></span>
                         <?php } ?>
                     </td>
                 </tr>
                 <tr>
-                    <td style="padding-left: 10px"><?php echo $plug['desc'] ?></td>
+                    <td style="padding-left: 10px"><?php echo $plug['description'] ?></td>
                 </tr>
-<?php if (count($pluginInfo) > 0) { ?>
+<?php       if (count($pluginInfo) > 0) { ?>
                 <tr>
                     <td style="padding-left: 10px; font-size: x-small"><?php echo implode('; ', $pluginInfo); ?></td>
                 </tr>
-<?php } ?>
+<?php       } ?>
             </table>
         </td>
     </tr>
 <?php
+        }
     }
-
-    echo $plugin_navigation;
 ?>
 </table>
 
Index: include/admin/templates.inc.php
===================================================================
--- include/admin/templates.inc.php	(revision 210)
+++ include/admin/templates.inc.php	(working copy)
@@ -11,6 +11,8 @@
 }
 
 if ($serendipity['GET']['adminAction'] == 'install' ) {
+    serendipity_plugin_api::hook_event('backend_templates_fetchtemplate', $serendipity);
+
     $themeInfo = serendipity_fetchTemplateInfo($serendipity['GET']['theme']);
 
     serendipity_set_config_var('template', $serendipity['GET']['theme']);
@@ -28,11 +30,19 @@
 
 
 <?php echo SELECT_TEMPLATE; ?>
+<br /><br />
 <?php
     $i = 0;
-    foreach (serendipity_fetchTemplates() as $theme) {
+    $stack = array();
+    serendipity_plugin_api::hook_event('backend_templates_fetchlist', $stack);
+    $themes = serendipity_fetchTemplates();
+    foreach($themes AS $theme) {
+        $stack[$theme] = serendipity_fetchTemplateInfo($theme);
+    }
+    ksort($stack);
+
+    foreach ($stack as $theme => $info) {
         $i++;
-        $info = serendipity_fetchTemplateInfo($theme);
 
         /* Sorry, but we don't display engines */
         if ( strtolower($info['engine']) == 'yes' ) {
@@ -42,9 +52,15 @@
 
         if (file_exists($serendipity['serendipityPath'] . $serendipity['templatePath'] . $theme . '/preview.png')) {
             $preview = '<img src="' . $serendipity['templatePath'] . $theme . '/preview.png" width="100" style="border: 1px #000000 solid" />';
+        } elseif (!empty($info['previewURL'])) {
+            $preview = '<img src="' . $info['previewURL'] . '" width="100" style="border: 1px #000000 solid" />';        
         } else {
             $preview = '&nbsp;';
         }
+        
+        if (empty($info['customURI'])) {
+            $info['customURI'] = '';
+        }
 
         $unmetRequirements = array();
         if ( isset($info['require serendipity']) && version_compare($info['require serendipity'], serendipity_getCoreVersion($serendipity['version']), '>') ) {
@@ -65,7 +81,7 @@
     if ( $serendipity['template'] != $theme ) {
         if ( !sizeof($unmetRequirements) ) {
 ?>
-            <a href="?serendipity[adminModule]=templates&amp;serendipity[adminAction]=install&amp;serendipity[theme]=<?php echo $theme ?>"><img src="<?php echo serendipity_getTemplateFile('admin/img/install_now.png') ?>" alt="<?php echo SET_AS_TEMPLATE ?>" title="<?php echo SET_AS_TEMPLATE ?>" border="0" /></a>
+            <a href="?serendipity[adminModule]=templates&amp;serendipity[adminAction]=install&amp;serendipity[theme]=<?php echo $theme . $info['customURI']; ?>"><img src="<?php echo serendipity_getTemplateFile('admin/img/install_now' . $info['customIcon'] . '.png') ?>" alt="<?php echo SET_AS_TEMPLATE ?>" title="<?php echo SET_AS_TEMPLATE ?>" border="0" /></a>
 <?php   } else { ?>
         <span style="color: #cccccc"><?php echo sprintf(UNMET_REQUIREMENTS, implode(', ', $unmetRequirements)); ?></span>
 <?php
Index: include/functions_images.inc.php
===================================================================
--- include/functions_images.inc.php	(revision 210)
+++ include/functions_images.inc.php	(working copy)
@@ -1190,7 +1190,7 @@
 }
 
 
-function serendipity_traversePath($basedir, $dir='', $onlyDirs=true, $pattern = NULL, $depth = 1) {
+function serendipity_traversePath($basedir, $dir='', $onlyDirs=true, $pattern = NULL, $depth = 1, $max_depth = null) {
 
 
     $dh = @opendir($basedir . '/' . $dir);
@@ -1210,8 +1210,8 @@
                     );
                 }
             }
-            if ( is_dir($basedir . '/' . $dir . '/' . $file) ) {
-                $files = array_merge($files, serendipity_traversePath($basedir, $dir . '/' . basename($file) . '/', $onlyDirs, $pattern, ($depth+1)));
+            if ( is_dir($basedir . '/' . $dir . '/' . $file) && ($max_depth === null || $depth < $max_depth)) {
+                $files = array_merge($files, serendipity_traversePath($basedir, $dir . '/' . basename($file) . '/', $onlyDirs, $pattern, ($depth+1), $max_depth));
             }
         }
     }
Index: include/functions.inc.php
===================================================================
--- include/functions.inc.php	(revision 210)
+++ include/functions.inc.php	(working copy)
@@ -96,10 +96,14 @@
     return $rv;
 }
 
-function serendipity_fetchTemplateInfo($theme) {
+function serendipity_fetchTemplateInfo($theme, $abspath = null) {
     global $serendipity;
 
-    $lines = @file($serendipity['serendipityPath'] . $serendipity['templatePath'] . $theme . '/info.txt');
+    if ($abspath === null) {
+        $abspath = $serendipity['serendipityPath'] . $serendipity['templatePath'];
+    }
+
+    $lines = @file($abspath . $theme . '/info.txt');
     if ( !$lines ) {
         return array();
     }
Index: docs/NEWS
===================================================================
--- docs/NEWS	(revision 210)
+++ docs/NEWS	(working copy)
@@ -3,6 +3,11 @@
 Version 0.9 ()
 ------------------------------------------------------------------------
 
+    * Templates can now be handled via Spartacus (garvinhicking)        
+    
+    * Plugin Manager: Improve Spartacus interface and include plugin      
+      categories (garvinhicking)
+
     * fixed serendipity_traversePath() -  PHP5 issue with array_merge()
       Thanks to jdhawk for the fix (flotsam)
 
@@ -92,9 +97,6 @@
       that within templates you can still use the $CONST shortcut
       (garvinhicking)
 
-    * Pagination of plugins to install to save memory allocation if many
-      plugins are downloaded (garvinhicking)
-
     * Added new event hook to the trackback sending facility so that
       plugins like serendipity_event_trackback (additional_plugins)
       can send trackbacks to Blogs without RDF-metadata (garvinhicking)
Index: templates/default/admin/style.css
===================================================================
--- templates/default/admin/style.css	(revision 210)
+++ templates/default/admin/style.css	(working copy)
@@ -255,3 +255,20 @@
 .direction_rtl {
     direction: rtl;
 }
+
+.serendipity_pluginlist_header {
+    height: 30px; 
+    background-color: white; 
+    color: black; 
+    vertical-align: middle; 
+    padding: 10px 0px 5px 10px;
+}
+
+.serendipity_pluginlist_section {
+    height: 50px; 
+    border: 1px solid black; 
+    background-color: #E0E0E0; 
+    color: black; 
+    vertical-align: middle; 
+    padding: 10px 0px 5px 10px;
+}
\ No newline at end of file
Index: sql/db_update_0.9-alpha3_0.9-alpha4_mysql.sql
===================================================================
--- sql/db_update_0.9-alpha3_0.9-alpha4_mysql.sql	(revision 0)
+++ sql/db_update_0.9-alpha3_0.9-alpha4_mysql.sql	(revision 0)
@@ -0,0 +1,29 @@
+create table {PREFIX}plugincategories (
+  class_name varchar(250) default null,
+  category varchar(250) default null
+);
+
+CREATE INDEX plugincat_idx ON {PREFIX}plugincategories(class_name, category);
+
+create table {PREFIX}pluginlist (
+  plugin_file varchar(255) NOT NULL default '',
+  class_name varchar(255) NOT NULL default '',
+  plugin_class varchar(255) NOT NULL default '',
+  pluginPath varchar(255) NOT NULL default '',
+  name varchar(255) NOT NULL default '',
+  description text NOT NULL,
+  version varchar(12) NOT NULL default '',
+  upgrade_version varchar(12) NOT NULL default '',
+  plugintype varchar(255) NOT NULL default '',
+  pluginlocation varchar(255) NOT NULL default '',
+  stackable int(1) NOT NULL default '0',
+  author varchar(255) NOT NULL default '',
+  requirements text NOT NULL,
+  website varchar(255) NOT NULL default '',
+  last_modified int(11) NOT NULL default '0'
+);
+
+CREATE INDEX pluginlist_f_idx ON {PREFIX}pluginlist(plugin_file);
+CREATE INDEX pluginlist_cn_idx ON {PREFIX}pluginlist(class_name);
+CREATE INDEX pluginlist_pt_idx ON {PREFIX}pluginlist(plugintype);
+CREATE INDEX pluginlist_pl_idx ON {PREFIX}pluginlist(pluginlocation);
Index: sql/db.sql
===================================================================
--- sql/db.sql	(revision 210)
+++ sql/db.sql	(working copy)
@@ -245,3 +245,33 @@
 CREATE INDEX ple_idx ON {PREFIX}permalinks (entry_id);
 CREATE INDEX plt_idx ON {PREFIX}permalinks (type);
 CREATE INDEX plcomb_idx ON {PREFIX}permalinks (permalink, type);
+
+create table {PREFIX}plugincategories (
+  class_name varchar(250) default null,
+  category varchar(250) default null
+);
+
+CREATE INDEX plugincat_idx ON {PREFIX}plugincategories(class_name, category);
+
+create table {PREFIX}pluginlist (
+  plugin_file varchar(255) NOT NULL default '',
+  class_name varchar(255) NOT NULL default '',
+  plugin_class varchar(255) NOT NULL default '',
+  pluginPath varchar(255) NOT NULL default '',
+  name varchar(255) NOT NULL default '',
+  description text NOT NULL,
+  version varchar(12) NOT NULL default '',
+  upgrade_version varchar(12) NOT NULL default '',
+  plugintype varchar(255) NOT NULL default '',
+  pluginlocation varchar(255) NOT NULL default '',
+  stackable int(1) NOT NULL default '0',
+  author varchar(255) NOT NULL default '',
+  requirements text NOT NULL,
+  website varchar(255) NOT NULL default '',
+  last_modified int(11) NOT NULL default '0'
+);
+
+CREATE INDEX pluginlist_f_idx ON {PREFIX}pluginlist(plugin_file);
+CREATE INDEX pluginlist_cn_idx ON {PREFIX}pluginlist(class_name);
+CREATE INDEX pluginlist_pt_idx ON {PREFIX}pluginlist(plugintype);
+CREATE INDEX pluginlist_pl_idx ON {PREFIX}pluginlist(pluginlocation);
