From 477d4e981a4171dcc8b7886b2130abbee0442067 Mon Sep 17 00:00:00 2001
From: tbrehm <t.brehm@ispconfig.org>
Date: Thu, 24 May 2012 07:26:50 -0400
Subject: [PATCH] Initial commit of the interface part of the APS installer.

---
 interface/web/sites/list/aps_availablepackages.list.php   |   86 +
 interface/web/sites/aps_installedpackages_list.php        |  127 ++
 install/dist/lib/fedora.lib.php                           |    4 
 interface/web/sites/lib/module.conf.php                   |   28 
 interface/lib/classes/aps_guicontroller.inc.php           |  770 ++++++++++++++++
 interface/web/sites/aps_install_package.php               |  198 ++++
 interface/web/sites/lib/lang/en_aps.lng                   |   57 +
 interface/web/themes/default/images/ajax-loader.gif       |    0 
 interface/web/sites/list/aps_installedpackages.list.php   |   81 +
 interface/web/sites/templates/aps_install_package.htm     |   54 +
 install/lib/installer_base.lib.php                        |    4 
 install/sql/ispconfig3.sql                                |   71 +
 interface/web/sites/templates/aps_packages_list.htm       |   48 +
 interface/web/sites/lib/lang/en_aps_packages_list.lng     |    8 
 interface/web/themes/default/css/screen/content_ispc.css  |    5 
 install/sql/incremental/upd_0034.sql                      |   76 +
 interface/lib/classes/aps_base.inc.php                    |  109 ++
 interface/web/sites/aps_cron_apscrawler_if.php            |   59 +
 interface/web/sites/templates/aps_instances_list.htm      |   60 +
 interface/web/js/scrigo.js.php                            |    5 
 interface/web/sites/lib/lang/en_aps_instances_list.lng    |   13 
 install/dist/lib/gentoo.lib.php                           |    4 
 install/dist/lib/opensuse.lib.php                         |    4 
 interface/web/sites/aps_do_operation.php                  |  110 ++
 interface/web/sites/aps_packagedetails_show.php           |   99 ++
 interface/web/sites/templates/aps_packagedetails_show.htm |  122 ++
 interface/lib/classes/aps_crawler.inc.php                 |  534 +++++++++++
 interface/web/sites/aps_availablepackages_list.php        |   56 +
 28 files changed, 2,790 insertions(+), 2 deletions(-)

diff --git a/install/dist/lib/fedora.lib.php b/install/dist/lib/fedora.lib.php
index ba1c285..f6ed418 100644
--- a/install/dist/lib/fedora.lib.php
+++ b/install/dist/lib/fedora.lib.php
@@ -865,6 +865,10 @@
 			}
 		}
 		
+		//* Make the APS directories group writable
+		exec("chmod -R 770 $install_dir/interface/web/sites/aps_meta_packages");
+		exec("chmod -R 770 $install_dir/server/aps_packages");
+		
 		//* make sure that the server config file (not the interface one) is only readable by the root user
 		exec("chmod 600 $install_dir/server/lib/$configfile");
 		exec("chown root:root $install_dir/server/lib/$configfile");
diff --git a/install/dist/lib/gentoo.lib.php b/install/dist/lib/gentoo.lib.php
index 5e5c93c..ad94bad 100644
--- a/install/dist/lib/gentoo.lib.php
+++ b/install/dist/lib/gentoo.lib.php
@@ -858,6 +858,10 @@
 			}
 		}
 		
+		//* Make the APS directories group writable
+		exec("chmod -R 770 $install_dir/interface/web/sites/aps_meta_packages");
+		exec("chmod -R 770 $install_dir/server/aps_packages");
+		
 	    //* make sure that the server config file (not the interface one) is only readable by the root user
     	chmod($install_dir.'/server/lib/'.$configfile, 0600);
 		chown($install_dir.'/server/lib/'.$configfile, 'root');
diff --git a/install/dist/lib/opensuse.lib.php b/install/dist/lib/opensuse.lib.php
index 88f508a..2dfcd70 100644
--- a/install/dist/lib/opensuse.lib.php
+++ b/install/dist/lib/opensuse.lib.php
@@ -903,6 +903,10 @@
 			}
 		}
 		
+		//* Make the APS directories group writable
+		exec("chmod -R 770 $install_dir/interface/web/sites/aps_meta_packages");
+		exec("chmod -R 770 $install_dir/server/aps_packages");
+		
 		//* make sure that the server config file (not the interface one) is only readable by the root user
 		exec("chmod 600 $install_dir/server/lib/$configfile");
 		exec("chown root:root $install_dir/server/lib/$configfile");
diff --git a/install/lib/installer_base.lib.php b/install/lib/installer_base.lib.php
index 94a176a..b472844 100644
--- a/install/lib/installer_base.lib.php
+++ b/install/lib/installer_base.lib.php
@@ -1767,6 +1767,10 @@
 				}
 			}
 		}
+		
+		//* Make the APS directories group writable
+		exec("chmod -R 770 $install_dir/interface/web/sites/aps_meta_packages");
+		exec("chmod -R 770 $install_dir/server/aps_packages");
 
 		//* make sure that the server config file (not the interface one) is only readable by the root user
 		chmod($install_dir.'/server/lib/'.$configfile, 0600);
diff --git a/install/sql/incremental/upd_0034.sql b/install/sql/incremental/upd_0034.sql
new file mode 100644
index 0000000..ea84577
--- /dev/null
+++ b/install/sql/incremental/upd_0034.sql
@@ -0,0 +1,76 @@
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `aps_instances`
+--
+
+CREATE TABLE IF NOT EXISTS `aps_instances` (
+  `id` int(4) NOT NULL AUTO_INCREMENT,
+  `sys_userid` int(11) unsigned NOT NULL DEFAULT '0',
+  `sys_groupid` int(11) unsigned NOT NULL DEFAULT '0',
+  `sys_perm_user` varchar(5) DEFAULT NULL,
+  `sys_perm_group` varchar(5) DEFAULT NULL,
+  `sys_perm_other` varchar(5) DEFAULT NULL,
+  `server_id` int(11) NOT NULL DEFAULT '0',
+  `customer_id` int(4) NOT NULL,
+  `package_id` int(4) NOT NULL,
+  `instance_status` int(4) NOT NULL,
+  PRIMARY KEY (`id`)
+) ENGINE=MyISAM DEFAULT CHARSET=utf8 ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `aps_instances_settings`
+--
+
+CREATE TABLE IF NOT EXISTS `aps_instances_settings` (
+  `id` int(4) NOT NULL AUTO_INCREMENT,
+  `server_id` int(11) NOT NULL DEFAULT '0',
+  `instance_id` int(4) NOT NULL,
+  `name` varchar(255) NOT NULL,
+  `value` text NOT NULL,
+  PRIMARY KEY (`id`)
+) ENGINE=MyISAM DEFAULT CHARSET=utf8 ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `aps_packages`
+--
+
+CREATE TABLE IF NOT EXISTS `aps_packages` (
+  `id` int(4) NOT NULL AUTO_INCREMENT,
+  `path` varchar(255) NOT NULL,
+  `name` varchar(255) NOT NULL,
+  `category` varchar(255) NOT NULL,
+  `version` varchar(20) NOT NULL,
+  `release` int(4) NOT NULL,
+  `package_status` int(1) NOT NULL DEFAULT '2',
+  PRIMARY KEY (`id`)
+) ENGINE=MyISAM DEFAULT CHARSET=utf8 ;
+
+-- --------------------------------------------------------
+
+--
+-- Tabellenstruktur für Tabelle `aps_settings`
+--
+
+CREATE TABLE IF NOT EXISTS `aps_settings` (
+  `id` int(4) NOT NULL AUTO_INCREMENT,
+  `name` varchar(255) NOT NULL,
+  `value` text NOT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `name` (`name`)
+) ENGINE=MyISAM DEFAULT CHARSET=utf8 ;
+
+--
+-- Dumping data for table `aps_settings`
+--
+
+INSERT INTO `aps_settings` (`id`, `name`, `value`) VALUES(1, 'ignore-php-extension', '');
+INSERT INTO `aps_settings` (`id`, `name`, `value`) VALUES(2, 'ignore-php-configuration', '');
+INSERT INTO `aps_settings` (`id`, `name`, `value`) VALUES(3, 'ignore-webserver-module', '');
+
+ALTER TABLE  `client` ADD  `limit_aps` int(11) NOT NULL DEFAULT '0' AFTER  `limit_webdav_user`;
+ALTER TABLE  `client_template` ADD  `limit_aps` int(11) NOT NULL DEFAULT '0' AFTER  `limit_webdav_user`;
\ No newline at end of file
diff --git a/install/sql/ispconfig3.sql b/install/sql/ispconfig3.sql
index 842d618..8a08f31 100644
--- a/install/sql/ispconfig3.sql
+++ b/install/sql/ispconfig3.sql
@@ -54,6 +54,67 @@
 -- --------------------------------------------------------
 
 --
+-- Table structure for table `aps_instances`
+--
+
+CREATE TABLE IF NOT EXISTS `aps_instances` (
+  `id` int(4) NOT NULL AUTO_INCREMENT,
+  `server_id` int(11) NOT NULL DEFAULT '0',
+  `customer_id` int(4) NOT NULL,
+  `package_id` int(4) NOT NULL,
+  `instance_status` int(4) NOT NULL,
+  PRIMARY KEY (`id`)
+) ENGINE=MyISAM DEFAULT CHARSET=utf8 ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `aps_instances_settings`
+--
+
+CREATE TABLE IF NOT EXISTS `aps_instances_settings` (
+  `id` int(4) NOT NULL AUTO_INCREMENT,
+  `server_id` int(11) NOT NULL DEFAULT '0',
+  `instance_id` int(4) NOT NULL,
+  `name` varchar(255) NOT NULL,
+  `value` text NOT NULL,
+  PRIMARY KEY (`id`)
+) ENGINE=MyISAM DEFAULT CHARSET=utf8 ;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `aps_packages`
+--
+
+CREATE TABLE IF NOT EXISTS `aps_packages` (
+  `id` int(4) NOT NULL AUTO_INCREMENT,
+  `path` varchar(255) NOT NULL,
+  `name` varchar(255) NOT NULL,
+  `category` varchar(255) NOT NULL,
+  `version` varchar(20) NOT NULL,
+  `release` int(4) NOT NULL,
+  `package_status` int(1) NOT NULL DEFAULT '2',
+  PRIMARY KEY (`id`)
+) ENGINE=MyISAM DEFAULT CHARSET=utf8 ;
+
+-- --------------------------------------------------------
+
+--
+-- Tabellenstruktur für Tabelle `aps_settings`
+--
+
+CREATE TABLE IF NOT EXISTS `aps_settings` (
+  `id` int(4) NOT NULL AUTO_INCREMENT,
+  `name` varchar(255) NOT NULL,
+  `value` text NOT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `name` (`name`)
+) ENGINE=MyISAM DEFAULT CHARSET=utf8 ;
+
+-- --------------------------------------------------------
+
+--
 -- Table structure for table `attempts_login`
 --
 
@@ -123,6 +184,7 @@
   `limit_shell_user` int(11) NOT NULL DEFAULT '0',
   `ssh_chroot` varchar(255) NOT NULL DEFAULT 'no,jailkit,ssh-chroot',
   `limit_webdav_user` int(11) NOT NULL DEFAULT '0',
+  `limit_aps` int(11) NOT NULL DEFAULT '0',
   `default_dnsserver` int(11) unsigned NOT NULL DEFAULT '1',
   `limit_dns_zone` int(11) NOT NULL DEFAULT '-1',
   `limit_dns_slave_zone` int(11) NOT NULL DEFAULT '-1',
@@ -208,6 +270,7 @@
   `limit_shell_user` int(11) NOT NULL default '0',
   `ssh_chroot` varchar(255) NOT NULL DEFAULT 'no',
   `limit_webdav_user` int(11) NOT NULL default '0',
+  `limit_aps` int(11) NOT NULL DEFAULT '0',
   `limit_dns_zone` int(11) NOT NULL default '-1',
   `limit_dns_slave_zone` int(11) NOT NULL default '-1',
   `limit_dns_record` int(11) NOT NULL default '-1',
@@ -1670,6 +1733,14 @@
 -- --------------------------------------------------------
 -- --------------------------------------------------------
 
+--
+-- Dumping data for table `aps_settings`
+--
+
+INSERT INTO `aps_settings` (`id`, `name`, `value`) VALUES(1, 'ignore-php-extension', '');
+INSERT INTO `aps_settings` (`id`, `name`, `value`) VALUES(2, 'ignore-php-configuration', '');
+INSERT INTO `aps_settings` (`id`, `name`, `value`) VALUES(3, 'ignore-webserver-module', '');
+
 -- --------------------------------------------------------
 
 --
diff --git a/interface/lib/classes/aps_base.inc.php b/interface/lib/classes/aps_base.inc.php
new file mode 100644
index 0000000..9822cae
--- /dev/null
+++ b/interface/lib/classes/aps_base.inc.php
@@ -0,0 +1,109 @@
+<?php
+/*
+Copyright (c) 2012, ISPConfig UG
+Contributors: web wack creations,  http://www.web-wack.at
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice,
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice,
+      this list of conditions and the following disclaimer in the documentation
+      and/or other materials provided with the distribution.
+    * Neither the name of ISPConfig nor the names of its contributors
+      may be used to endorse or promote products derived from this software without
+      specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+// Constants describing instances
+define('INSTANCE_PENDING', 0);
+define('INSTANCE_INSTALL', 1);
+define('INSTANCE_ERROR', 2);
+define('INSTANCE_SUCCESS', 3);
+define('INSTANCE_REMOVE', 4);
+
+// Constants describing packages
+define('PACKAGE_LOCKED', 1);
+define('PACKAGE_ENABLED', 2);
+define('PACKAGE_OUTDATED', 3);
+define('PACKAGE_ERROR_NOMETA', 4);
+
+class ApsBase
+{
+    protected $app = null;
+    protected $db = null;
+    
+    protected $log_prefix = '';
+    protected $fetch_url = '';
+    protected $aps_version = '';
+    protected $packages_dir = '';
+    protected $temp_pkg_dir = '';
+    protected $interface_pkg_dir = '';
+    protected $interface_mode = false; // server mode by default
+
+    /**
+     * Constructor
+     *
+     * @param $app the application instance (db handle + log method)
+     * @param $interface_mode act in interface (true) or server mode (false)
+     * @param $log_prefix a prefix to set before all log entries
+     */
+    public function __construct($app, $log_prefix = 'APS: ', $interface_mode = false)
+    {
+        $this->db = $app->db;
+        $this->app = $app;
+        
+        $this->log_prefix = $log_prefix;
+        $this->interface_mode = $interface_mode;
+        $this->fetch_url = 'apscatalog.com';
+        $this->aps_version = '1';
+        $this->packages_dir = ISPC_ROOT_PATH.'/aps_packages';
+        $this->interface_pkg_dir = ISPC_ROOT_PATH.'/web/sites/aps_meta_packages';
+    }
+    
+    /**
+     * Converts a given value to it's native representation in 1024 units
+     * 
+     * @param $value the size to convert
+     * @return integer and string 
+     */
+    public function convertSize($value)
+    {
+        $unit = array('Bytes', 'KB', 'MB', 'GB', 'TB');
+        return @round($value/pow(1024, ($i = floor(log($value, 1024)))), 2).' '.$unit[$i];
+    }
+    
+    /**
+     * Determine a specific xpath from a given SimpleXMLElement handle. If the
+     * element is found, it's string representation is returned. If not,
+     * the return value will stay empty
+     *
+     * @param $xml_handle the SimpleXMLElement handle
+     * @param $query the XPath query
+     * @param $array define whether to return an array or a string
+     * @return $ret the return string
+     */
+    protected function getXPathValue($xml_handle, $query, $array = false)
+    {
+        $ret = '';
+        
+        $xp_result = @($xml_handle->xpath($query)) ? $xml_handle->xpath($query) : false;
+        if($xp_result !== false) $ret = (($array === false) ? (string)$xp_result[0] : $xp_result);
+        
+        return $ret;
+    }
+}
+?>
\ No newline at end of file
diff --git a/interface/lib/classes/aps_crawler.inc.php b/interface/lib/classes/aps_crawler.inc.php
new file mode 100644
index 0000000..640bb34
--- /dev/null
+++ b/interface/lib/classes/aps_crawler.inc.php
@@ -0,0 +1,534 @@
+<?php
+/*
+Copyright (c) 2012, ISPConfig UG
+Contributors: web wack creations,  http://www.web-wack.at
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice,
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice,
+      this list of conditions and the following disclaimer in the documentation
+      and/or other materials provided with the distribution.
+    * Neither the name of ISPConfig nor the names of its contributors
+      may be used to endorse or promote products derived from this software without
+      specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+require_once('aps_base.inc.php');
+
+@set_time_limit(0);
+@ignore_user_abort(1);
+
+class ApsCrawler extends ApsBase
+{
+   /**
+    * Constructor
+    *
+    * @param $app the application instance (db handle + log method)
+    * @param $interface_mode act in interface (true) or server mode (false)
+    */
+    public function __construct($app, $interface_mode = false)
+    {
+        parent::__construct($app, 'APS crawler: ', $interface_mode);
+    }
+    
+    /**
+     * Before the cron is executed, make sure all necessary options are set
+     * and all functions (i.e. cURL) are available
+     */
+    private function checkRequirements()
+    {
+        try
+        {
+            // Check if allow_url_fopen is enabled
+            if(!@ini_get('allow_url_fopen')) throw new Exception('allow_url_fopen is not enabled');
+            // Check if the cURL module is available
+            if(!function_exists('curl_version')) throw new Exception('cURL is not available');
+            
+            // Check if used folders are writable (chmod 777)
+            if($this->interface_mode)
+            {
+                if(!is_writable($this->interface_pkg_dir)) 
+                    throw new Exception('the folder '.basename($this->interface_pkg_dir).' is not writable');  
+            }   
+            else 
+            {
+                if(!is_writable($this->packages_dir)) 
+                    throw new Exception('the folder '.basename($this->packages_dir).' is not writable');
+            }
+            
+            return true;
+        }
+        catch(Exception $e)
+        {
+            $this->app->log($this->log_prefix.'Aborting execution because '.$e->getMessage(), LOGLEVEL_ERROR);
+            return false;
+        }
+    }
+    
+    /**
+     * Remove a directory recursively
+     * In case of error be silent
+     * 
+     * @param $dir the directory to remove
+     */
+    private function removeDirectory($dir)
+    {
+        if(is_dir($dir))
+        {
+            $files = scandir($dir);
+            foreach($files as $file)
+            {
+                if($file != '.' && $file != '..')
+                    if(filetype($dir.'/'.$file) == 'dir') rrmdir($dir.'/'.$file); 
+                    else @unlink($dir.'/'.$file);
+            }
+            reset($files);
+            @rmdir($dir);
+        }
+    }
+
+    
+    /**
+     * Fetch HTML data from one or more given URLs
+     * If a string is given, a string is returned, if an array of URLs should
+     * be fetched, the responses of the parallel queries are returned as array
+     *
+     * @param $input the string or array to fetch
+     * @return $ret a query response string or array
+     */
+    private function fetchPage($input)
+    {
+        $ret = array();
+        $url = array();
+        $conn = array();
+
+        // Make sure we are working with an array, further on
+        if(!is_array($input)) $url[] = $input;
+        else $url = $input;
+        
+        // Build the single cURL handles and add them to a multi handle
+        $mh = curl_multi_init();
+        for($i = 0; $i < count($url); $i++)
+        {
+            $conn[$i] = curl_init('http://'.$this->fetch_url.$url[$i]);
+            curl_setopt($conn[$i], CURLOPT_RETURNTRANSFER, true);
+            curl_multi_add_handle($mh, $conn[$i]);
+        }
+        
+        $active = 0;
+        do curl_multi_exec($mh, $active);
+        while($active > 0);
+
+        // Get the response(s)
+        for($i = 0; $i < count($url); $i++)
+        {
+            $ret[$i] = curl_multi_getcontent($conn[$i]);
+            curl_multi_remove_handle($mh, $conn[$i]);
+            curl_close($conn[$i]);
+        }
+        curl_multi_close($mh);
+        
+        if(count($url) == 1) $ret = $ret[0];
+        
+        return $ret;
+    }
+    
+    /**
+     * Fetch binary data from a given array
+     * The data is retrieved in binary mode and 
+     * then directly written to an output file
+     *
+     * @param $input a specially structed array
+     * @see $this->startUpdate()
+     */
+    private function fetchFiles($input)
+    {
+        $fh = array();
+        $url = array();
+        $conn = array();
+
+        // Build the single cURL handles and add them to a multi handle
+        $mh = curl_multi_init();
+        
+        // Process each app 
+        for($i = 0; $i < count($input); $i++)
+        {
+            $conn[$i] = curl_init($input[$i]['url']);
+            $fh[$i] = fopen($input[$i]['localtarget'], 'wb'); 
+            
+            curl_setopt($conn[$i], CURLOPT_BINARYTRANSFER, true);
+            curl_setopt($conn[$i], CURLOPT_FILE, $fh[$i]);
+            curl_setopt($conn[$i], CURLOPT_TIMEOUT, 0);
+            curl_setopt($conn[$i], CURLOPT_FAILONERROR, 1);
+            curl_setopt($conn[$i], CURLOPT_FOLLOWLOCATION, 1); 
+            
+            curl_multi_add_handle($mh, $conn[$i]);
+        }
+        
+        $active = 0;
+        do curl_multi_exec($mh, $active);
+        while($active > 0);
+
+        // Close the handles
+        for($i = 0; $i < count($input); $i++)
+        {
+            fclose($fh[$i]);
+            curl_multi_remove_handle($mh, $conn[$i]);
+            curl_close($conn[$i]);
+        }
+        curl_multi_close($mh);
+    }
+    
+    /**
+     * A method to build query URLs out of a list of vendors
+     *
+    */
+    private function formatVendorCallback(&$array_item, $key)
+    {
+        $array_item = str_replace(' ', '%20', $array_item);
+        $array_item = str_replace('http://', '', $array_item);
+        $array_item = '/'.$this->aps_version.'.atom?vendor='.$array_item.'&pageSize=100';
+    }
+    
+    /**
+     * The main method which performs the actual crawling
+     */    
+    public function startCrawler() 
+    {
+        try
+        {
+            // Make sure the requirements are given so that this script can execute
+            $req_ret = $this->checkRequirements();
+            if(!$req_ret) return false;
+            
+            // Execute the open task and first fetch all vendors (APS catalog API 1.1, p. 12)
+            $this->app->log($this->log_prefix.'Fetching data from '.$this->fetch_url);
+
+            $vendor_page = $this->fetchPage('/all-app/'); //$vendor_page = $this->fetchPage('/'.$this->aps_version.'/');
+            preg_match_all("/\<a href=\"(.+)\/\" class=\"vendor\"/i", $vendor_page, $matches);
+            $vendors = array_map('urldecode', $matches[1]);
+            if(!$vendors) throw new Exception('Unable to fetch vendors. Aborting');
+
+            // Format all vendors for further processing (i.e. typo3.org -> /1.atom?vendor=typo3.org&pageSize=100
+            array_walk($vendors, array($this, 'formatVendorCallback'));
+            
+            // Process all vendors in chunks of 50 entries
+            $vendor_chunks = array_chunk($vendors, 50);
+            //var_dump($vendor_chunks); 
+
+            // Get all known apps from the database and the highest known version
+            // Note: A dirty hack is used for numerical sorting of the VARCHAR field Version: +0 -> cast
+            // A longer but typesafe way would be: ORDER BY CAST(REPLACE(Version, '.', '') AS UNSIGNED) DESC
+            $existing_apps = $this->db->queryAllRecords("SELECT * FROM (
+                SELECT name AS Name, CONCAT(version, '-', CAST(`release` AS CHAR)) AS CurrentVersion 
+                FROM aps_packages ORDER BY REPLACE(version, '.', '')+0 DESC, `release` DESC
+                ) as Versions GROUP BY name");
+            //var_dump($existing_apps); 
+            
+            // Used for statistics later
+            $apps_in_repo = 0; 
+            $apps_updated = 0;
+            $apps_downloaded = 0;
+            
+            $apps_to_dl = array();
+            
+            for($i = 0; $i < count($vendor_chunks); $i++)
+            {
+                // Fetch all apps for the current chunk of vendors
+                $apps = $this->fetchPage($vendor_chunks[$i]);
+                
+                for($j = 0; $j < count($apps); $j++)
+                {
+                    // Before parsing, make sure it's worth the work by checking if at least one app exists
+                    $apps_count = substr_count($apps[$j], '<opensearch:totalResults>0</opensearch:totalResults>');
+                    if($apps_count == 0) // obviously this vendor provides one or more apps
+                    {
+                        // Rename namespaces and register them 
+                        $xml = str_replace("xmlns=", "ns=", $apps[$j]);
+                        $sxe = new SimpleXMLElement($xml);
+                        $namespaces = $sxe->getDocNamespaces(true);
+                        foreach($namespaces as $ns => $url) $sxe->registerXPathNamespace($ns, $url);
+                        
+                        // Fetching values of interest
+                        $app_name = parent::getXPathValue($sxe, 'entry[position()=1]/a:name');
+                        $app_version = parent::getXPathValue($sxe, 'entry[position()=1]/a:version');
+                        $app_release = parent::getXPathValue($sxe, 'entry[position()=1]/a:release');
+                        
+                        // Find out a (possibly) existing package version
+                        $ex_ver = '';
+                        array_walk($existing_apps, 
+                            create_function('$v, $k, $ex_ver', 'if($v["Name"] == "'.$app_name.'") $ex_ver = $v["CurrentVersion"];'), &$ex_ver);
+                        
+                        $new_ver = $app_version.'-'.$app_release;
+                        $local_intf_folder = $this->interface_pkg_dir.'/'.$app_name.'-'.$new_ver.'.app.zip/';
+
+                        // Proceed if a newer or at least equal version has been found with server mode or 
+                        // interface mode is activated and there's no valid APP-META.xml existing yet
+                        if((!$this->interface_mode && version_compare($new_ver, $ex_ver) >= 0)
+                         || ($this->interface_mode 
+                              && (!file_exists($local_intf_folder.'APP-META.xml') || filesize($local_intf_folder.'APP-META.xml') == 0)
+                            )
+                        )
+                        {
+                            // Check if we already have an old version of this app
+                            if(!empty($ex_ver) && version_compare($new_ver, $ex_ver) == 1) $apps_updated++; 
+
+                            $app_dl = parent::getXPathValue($sxe, "entry[position()=1]/link[@a:type='aps']/@href");
+                            $app_filesize = parent::getXPathValue($sxe, "entry[position()=1]/link[@a:type='aps']/@length");
+                            $app_metafile = parent::getXPathValue($sxe, "entry[position()=1]/link[@a:type='meta']/@href");
+
+                            // Skip ASP.net packages because they can't be used at all
+                            $asp_handler = parent::getXPathValue($sxe, '//aspnet:handler');
+                            $asp_permissions = parent::getXPathValue($sxe, '//aspnet:permissions');
+                            $asp_version = parent::getXPathValue($sxe, '//aspnet:version');
+                            if(!empty($asp_handler) || !empty($asp_permissions) || !empty($asp_version)) continue;
+
+                            // Interface mode (download only parts)
+                            if($this->interface_mode)
+                            {
+                                // Delete an obviously out-dated version from the system and DB
+                                if(!empty($ex_ver) && version_compare($new_ver, $ex_ver) == 1)
+                                {
+                                    $old_folder = $this->interface_pkg_dir.'/'.$app_name.'-'.$ex_ver.'.app.zip';
+                                    if(file_exists($old_folder)) $this->removeDirectory($old_folder);
+                                    
+									/*
+                                    $this->db->query("UPDATE aps_packages SET package_status = '".PACKAGE_OUTDATED."' WHERE name = '".
+                                        $this->db->quote($app_name)."' AND CONCAT(version, '-', CAST(`release` AS CHAR)) = '".
+                                        $this->db->quote($ex_ver)."';");
+									*/
+									$tmp = $this->db->queryOneRecord("SELECT id FROM aps_packages WHERE name = '".
+                                        $this->db->quote($app_name)."' AND CONCAT(version, '-', CAST(`release` AS CHAR)) = '".
+                                        $this->db->quote($ex_ver)."';");
+									$this->db->datalogUpdate('aps_packages', "package_status = ".PACKAGE_OUTDATED, 'id', $tmp['id']);
+									unset($tmp);
+                                }
+                                
+                                // Create the local folder if not yet existing
+                                if(!file_exists($local_intf_folder)) @mkdir($local_intf_folder, 0777, true);
+                                
+                                // Download the meta file
+                                $local_metafile = $local_intf_folder.'APP-META.xml';
+                                if(!file_exists($local_metafile) || filesize($local_metafile) == 0) 
+                                {
+                                    $apps_to_dl[] = array('name' => 'APP-META.xml', 
+                                                          'url' => $app_metafile, 
+                                                          'filesize' => 0, 
+                                                          'localtarget' => $local_metafile);
+                                    $apps_downloaded++;
+                                }
+                                
+                                // Download package license
+                                $license = parent::getXPathValue($sxe, "entry[position()=1]/link[@a:type='eula']/@href");
+                                if($license != '')
+                                {
+                                    $local_license = $local_intf_folder.'LICENSE';
+                                    if(!file_exists($local_license) || filesize($local_license) == 0)
+                                    {
+                                        $apps_to_dl[] = array('name' => basename($license), 
+                                                              'url' => $license, 
+                                                              'filesize' => 0, 
+                                                              'localtarget' => $local_license);
+                                    }
+                                }
+                                
+                                // Download package icon
+                                $icon = parent::getXPathValue($sxe, "entry[position()=1]/link[@a:type='icon']/@href");
+                                if($icon != '')
+                                {
+                                    $local_icon = $local_intf_folder.basename($icon);
+                                    if(!file_exists($local_icon) || filesize($local_icon) == 0)
+                                    {
+                                        $apps_to_dl[] = array('name' => basename($icon), 
+                                                              'url' => $icon, 
+                                                              'filesize' => 0, 
+                                                              'localtarget' => $local_icon);
+                                    }
+                                }
+                                
+                                // Download available screenshots
+                                $screenshots = parent::getXPathValue($sxe, "entry[position()=1]/link[@a:type='screenshot']", true);
+                                if(!empty($screenshots))
+                                {
+                                    foreach($screenshots as $screen)
+                                    {
+                                        $local_screen = $local_intf_folder.basename($screen['href']);
+                                        if(!file_exists($local_screen) || filesize($local_screen) == 0)
+                                        {
+                                            $apps_to_dl[] = array('name' => basename($screen['href']), 
+                                                                  'url' => $screen['href'], 
+                                                                  'filesize' => 0, 
+                                                                  'localtarget' => $local_screen);
+                                        }
+                                    }
+                                }
+                            }
+                            else // Server mode (download whole ZIP archive)
+                            {
+                                // Delete an obviously out-dated version from the system
+                                if(!empty($ex_ver) && version_compare($new_ver, $ex_ver) == 1)
+                                {
+                                    $old_file = $this->packages_dir.'/'.$app_name.'-'.$ex_ver.'.app.zip';
+                                    if(file_exists($old_file)) $this->removeDirectory($old_file);
+                                }
+                                
+                                // Attention: $new_ver can also be == $ex_ver (according to version_compare >= 0)
+                                $local_zip = $this->packages_dir.'/'.$app_name.'-'.$new_ver.'.app.zip';
+                            
+                                // Before re-downloading a file, make sure it's not yet existing on HDD (due to DB inconsistency)
+                                if((file_exists($local_zip) && (filesize($local_zip) == $app_filesize)) === false)
+                                {
+                                    $apps_to_dl[] = array('name' => $app_name, 
+                                                          'url' => $app_dl, 
+                                                          'filesize' => $app_filesize, 
+                                                          'localtarget' => $local_zip);
+                                    $apps_downloaded++;
+                                }
+                            }
+                        }
+                        
+                        unset($sxe);
+                        $apps_in_repo++;
+                    }
+                }
+                //var_dump($apps);
+                
+                // For memory reasons, unset the current vendor and his apps
+                unset($apps);
+            }
+            
+            // Shuffle the download array (in order to compensate unexpected php aborts)
+            shuffle($apps_to_dl);
+            
+            // After collecting all provisioned apps, download them
+            $apps_to_dl_chunks = array_chunk($apps_to_dl, 10);
+
+            for($i = 0; $i < count($apps_to_dl_chunks); $i++)
+            {
+                $this->fetchFiles($apps_to_dl_chunks[$i]);
+                
+                // Check the integrity of all downloaded files
+                // but exclude cases where no filesize is available (i.e. screenshot or metafile download)
+                for($j = 0; $j < count($apps_to_dl_chunks[$i]); $j++)
+                {
+                    if($apps_to_dl_chunks[$i][$j]['filesize'] != 0 &&
+                       $apps_to_dl_chunks[$i][$j]['filesize'] != filesize($apps_to_dl_chunks[$i][$j]['localtarget']))
+                    {
+                            $this->app->log($this->log_prefix.' The filesize of the package "'.
+                                $apps_to_dl_chunks[$i][$j]['name'].'" is wrong. Download failure?', LOGLEVEL_WARN);
+                    }
+                }
+            }
+            
+            $this->app->log($this->log_prefix.'Processed '.$apps_in_repo.
+                ' apps from the repo. Downloaded '.$apps_updated.
+                ' updates, '.$apps_downloaded.' new apps');
+        }
+        catch(Exception $e)
+        {
+            $this->app->log($this->log_prefix.$e->getMessage(), LOGLEVEL_ERROR);
+            return false;
+        }
+    }
+    
+    /**
+     * Read in all possible packages from the interface packages folder and 
+     * check if they are not ASP.net code (as this can't be processed).
+     * 
+     * Note: There's no need to check if the packages to register are newer
+     * than those in the database because this already happended in startCrawler()
+     */
+    public function parseFolderToDB()
+    {
+        try
+        {
+            // This method must be used in server mode
+            if(!$this->interface_mode) return false; 
+            
+            $pkg_list = array();
+        
+            // Read in every package having a correct filename
+            $temp_handle = @dir($this->interface_pkg_dir);
+            if(!$temp_handle) throw new Exception('The temp directory is not accessible');
+            while($folder = $temp_handle->read()) 
+                if(substr($folder, -8) == '.app.zip') $pkg_list[] = $folder;
+            $temp_handle->close();
+            
+            // If no packages are available -> exception (because at this point there should exist packages)
+            if(empty($pkg_list)) throw new Exception('No packages to read in');
+            
+            // Get registered packages and mark non-existant packages with an error code to omit the install
+            $existing_packages = array();
+            $path_query = $this->db->queryAllRecords('SELECT path AS Path FROM aps_packages;');
+            foreach($path_query as $path) $existing_packages[] = $path['Path']; 
+            $diff = array_diff($existing_packages, $pkg_list);
+            foreach($diff as $todelete)
+                /*$this->db->query("UPDATE aps_packages SET package_status = '".PACKAGE_ERROR_NOMETA."' 
+                    WHERE path = '".$this->db->quote($todelete)."';");*/
+				$tmp = $this->db->queryOneRecord("SELECT id FROM aps_packages WHERE path = '".$this->db->quote($todelete)."';");
+				$this->db->datalogUpdate('aps_packages', "package_status = ".PACKAGE_ERROR_NOMETA, 'id', $tmp['id']);
+				unset($tmp);
+            
+            // Register all new packages
+            $new_packages = array_diff($pkg_list, $existing_packages);
+            foreach($new_packages as $pkg)
+            {
+                // Load in meta file if existing and register its namespaces
+                $metafile = $this->interface_pkg_dir.'/'.$pkg.'/APP-META.xml';
+                if(!file_exists($metafile)) 
+                {
+                    $this->app->log($this->log_prefix.'Cannot read metadata from '.$pkg, LOGLEVEL_ERROR);
+                    continue;
+                }
+        
+                $metadata = file_get_contents($metafile);
+                $metadata = str_replace("xmlns=", "ns=", $metadata);
+                $sxe = new SimpleXMLElement($metadata);
+                $namespaces = $sxe->getDocNamespaces(true);
+                foreach($namespaces as $ns => $url) $sxe->registerXPathNamespace($ns, $url);
+                
+                // Insert the new package
+                $pkg_name = parent::getXPathValue($sxe, 'name');
+                $pkg_category = parent::getXPathValue($sxe, '//category');
+                $pkg_version = parent::getXPathValue($sxe, 'version');
+                $pkg_release = parent::getXPathValue($sxe, 'release');
+                
+				/*
+                $this->db->query("INSERT INTO `aps_packages` 
+                    (`path`, `name`, `category`, `version`, `release`, `package_status`) VALUES 
+                    ('".$this->db->quote($pkg)."', '".$this->db->quote($pkg_name)."',
+                    '".$this->db->quote($pkg_category)."', '".$this->db->quote($pkg_version)."',
+                    ".$this->db->quote($pkg_release).", ".PACKAGE_ENABLED.");");
+				*/
+				
+				$insert_data = "(`path`, `name`, `category`, `version`, `release`, `package_status`) VALUES 
+                    ('".$this->db->quote($pkg)."', '".$this->db->quote($pkg_name)."',
+                    '".$this->db->quote($pkg_category)."', '".$this->db->quote($pkg_version)."',
+                    ".$this->db->quote($pkg_release).", ".PACKAGE_ENABLED.");";
+				
+				$app->db->datalogInsert('aps_packages', $insert_data, 'id');
+            }
+        }
+        catch(Exception $e)
+        {
+            $this->app->log($this->log_prefix.$e->getMessage(), LOGLEVEL_ERROR);
+			$this->app->error($e->getMessage());
+            return false;
+        }
+    }
+}
+?>
\ No newline at end of file
diff --git a/interface/lib/classes/aps_guicontroller.inc.php b/interface/lib/classes/aps_guicontroller.inc.php
new file mode 100644
index 0000000..0b4038f
--- /dev/null
+++ b/interface/lib/classes/aps_guicontroller.inc.php
@@ -0,0 +1,770 @@
+<?php
+/*
+Copyright (c) 2012, ISPConfig UG
+Contributors: web wack creations,  http://www.web-wack.at
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice,
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice,
+      this list of conditions and the following disclaimer in the documentation
+      and/or other materials provided with the distribution.
+    * Neither the name of ISPConfig nor the names of its contributors
+      may be used to endorse or promote products derived from this software without
+      specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+require_once('aps_base.inc.php');
+
+class ApsGUIController extends ApsBase
+{
+    /**
+    * Constructor
+    *
+    * @param $app the application instance (db handle)
+    */
+    public function __construct($app)
+    {
+        parent::__construct($app);
+    }
+    
+    /**
+     * Reads in a package metadata file and registers it's namespaces
+     * 
+     * @param $filename the file to read
+     * @return $sxe a SimpleXMLElement handle
+     */
+    private function readInMetaFile($filename)
+    {
+        $metadata = file_get_contents($filename);
+        $metadata = str_replace("xmlns=", "ns=", $metadata);
+        $sxe = new SimpleXMLElement($metadata);
+        $namespaces = $sxe->getDocNamespaces(true);
+        foreach($namespaces as $ns => $url) $sxe->registerXPathNamespace($ns, $url);
+        
+        return $sxe; 
+    }
+    
+    /**
+     * Applies a RegEx pattern onto a location path in order to secure it against 
+     * code injections and invalid input
+     * 
+     * @param $location_unfiltered the file path to secure
+     * @return $location
+     */
+    private function secureLocation($location_unfiltered)
+    {
+        // Filter invalid slashes from string
+        $location = preg_replace(array('#/+#', '#\.+#', '#\0+#', '#\\\\+#'), 
+                                 array('/', '', '', '/'), 
+                                 $location_unfiltered);
+
+        // Remove a beginning or trailing slash
+        if(substr($location, -1) == '/') $location = substr($location, 0, strlen($location) - 1);
+        if(substr($location, 0, 1) == '/') $location = substr($location, 1);
+        
+        return $location;
+    }
+    
+    /**
+     * Gets the CustomerID (ClientID) which belongs to a specific domain
+     * 
+     * @param $domain the domain
+     * @return $customerid
+     */
+    private function getCustomerIDFromDomain($domain)
+    {
+        $customerid = '';
+        
+        $customerdata = $this->db->queryOneRecord("SELECT client_id FROM sys_group, web_domain
+            WHERE web_domain.sys_groupid = sys_group.groupid 
+            AND web_domain.domain = '".$this->db->quote($domain)."';");
+        if(!empty($customerdata)) $customerid = $customerdata['client_id'];
+        
+        return $customerid;
+    }
+    
+    /**
+     * Returns the server_id for an already installed instance. Is actually 
+     * just a little helper method to avoid redundant code
+     * 
+     * @param $instanceid the instance to process
+     * @return $webserver_id the server_id
+     */
+    private function getInstanceDataForDatalog($instanceid)
+    {
+        $webserver_id = '';
+        
+        $websrv = $this->db->queryOneRecord("SELECT server_id FROM web_domain 
+            WHERE domain = (SELECT value FROM aps_instances_settings 
+                WHERE name = 'main_domain' AND instance_id = ".$this->db->quote($instanceid).");");
+
+        // If $websrv is empty, an error has occured. Domain no longer existing? Settings table damaged?
+        // Anyhow, remove this instance record because it's not useful at all
+        if(empty($websrv)) 
+        {
+            $this->db->query("DELETE FROM aps_instances WHERE id = ".$this->db->quote($instanceid).";");
+            $this->db->query("DELETE FROM aps_instances_settings WHERE instance_id = ".$this->db->quote($instanceid).";");
+        }
+        else $webserver_id = $websrv['server_id'];
+        
+        return $webserver_id;
+    } 
+    
+    /**
+     * Finds out if there is a newer package version for 
+     * a given (possibly valid) package ID
+     * 
+     * @param $id the ID to check
+     * @return $newer_pkg_id the newer package ID
+     */ 
+    public function getNewestPackageID($id)
+    {
+        if(preg_match('/^[0-9]+$/', $id) != 1) return 0;
+        
+        $result = $this->db->queryOneRecord("SELECT id, name, 
+            CONCAT(version, '-', CAST(`release` AS CHAR)) AS current_version 
+            FROM aps_packages 
+            WHERE name = (SELECT name FROM aps_packages WHERE id = ".$this->db->quote($id).")  
+            ORDER BY REPLACE(version, '.', '')+0 DESC, `release` DESC");
+            
+        if(!empty($result) && ($id != $result['id'])) return $result['id'];
+        
+        return 0; 
+    }
+
+    /**
+     * Validates a given package ID
+     *
+     * @param $id the ID to check
+     * @param $is_admin a flag to allow locked IDs too (for admin calls)
+     * @return boolean
+     */
+    public function isValidPackageID($id, $is_admin = false)
+    {
+         if(preg_match('/^[0-9]+$/', $id) != 1) return false;
+         
+         $sql_ext = (!$is_admin) ? 
+            'package_status = '.PACKAGE_ENABLED.' AND' :  
+            '(package_status = '.PACKAGE_ENABLED.' OR package_status = '.PACKAGE_LOCKED.') AND'; 
+
+         $result = $this->db->queryOneRecord("SELECT id FROM aps_packages WHERE ".$sql_ext." id = ".$this->db->quote($id).";");
+         if(!$result) return false;
+         
+         return true;
+    }
+    
+    /**
+     * Validates a given instance ID
+     *
+     * @param $id the ID to check
+     * @param $client_id the calling client ID
+     * @param $is_admin a flag to ignore the client ID check for admins
+     * @return boolean
+     */
+    public function isValidInstanceID($id, $client_id, $is_admin = false)
+    {
+         if(preg_match('/^[0-9]+$/', $id) != 1) return false;
+         
+         // Only filter if not admin
+         $sql_ext = (!$is_admin) ? 'customer_id = '.$this->db->quote($client_id).' AND' : ''; 
+
+         $result = $this->db->queryOneRecord('SELECT id FROM aps_instances WHERE '.$sql_ext.' id = '.$this->db->quote($id).';');
+         if(!$result) return false;
+         
+         return true;
+    }    
+    
+    /**
+     * Creates a new database record for the package instance and
+     * an install task
+     * 
+     * @param $settings the settings to enter into the DB
+     * @param $packageid the PackageID
+     */
+    public function createPackageInstance($settings, $packageid)
+    {
+		global $app;
+		
+		$webserver_id = 0;
+        $websrv = $this->db->queryOneRecord("SELECT * FROM web_domain WHERE domain = '".$this->db->quote($settings['main_domain'])."';");
+        if(!empty($websrv)) $webserver_id = $websrv['server_id'];
+        $customerid = $this->getCustomerIDFromDomain($settings['main_domain']);
+        
+        if(empty($settings) || empty($customerid) || empty($webserver_id)) return false;
+		
+		//* Get server config of the web server
+		$this->app->uses("getconf");
+		$web_config = $this->app->getconf->get_server_config(intval($websrv["server_id"]),'web');
+			
+		//* Set mysql mode to php-fcgi and enable suexec in website on apache servers
+		if($web_config['server_type'] == 'apache') {
+			if($websrv['php'] != 'fast-cgi' || $websrv['suexec'] != 'y') {
+				$app->db->datalogUpdate('web_domain', "php = 'fast-cgi', suexec = 'y'", 'domain_id', $websrv['domain_id']);
+			}
+		}
+		
+		//* Create the MySQL database for the application
+		$pkg = $this->db->queryOneRecord('SELECT * FROM aps_packages WHERE id = '.$this->db->quote($packageid).';');
+		$metafile = $this->interface_pkg_dir.'/'.$pkg['path'].'/APP-META.xml';
+		$sxe = $this->readInMetaFile($metafile);
+		
+		$db_id = parent::getXPathValue($sxe, '//db:id');
+		if (!empty($db_id)) {
+			$global_config = $app->getconf->get_global_config('sites');
+			
+			$tmp = array();
+			$tmp['parent_domain_id'] = $websrv['domain_id'];
+			$tmp['sys_groupid'] = $websrv['sys_groupid'];
+			$dbname_prefix = replacePrefix($global_config['dbname_prefix'], $tmp);
+			$dbuser_prefix = replacePrefix($global_config['dbuser_prefix'], $tmp);
+			unset($tmp);
+			
+			//* get the default database server of the client
+			$client = $app->db->queryOneRecord("SELECT default_dbserver FROM sys_group, client WHERE sys_group.client_id = client.client_id and sys_group.groupid = ".$websrv['sys_groupid']);
+			if(is_array($client) && $client['default_dbserver'] > 0 && $client['default_dbserver'] != $websrv['server_id']) {
+				$mysql_db_server_id =  $client['default_dbserver'];
+				$dbserver_config = $web_config = $app->getconf->get_server_config(intval($mysql_db_server_id),'server');
+				$mysql_db_host = $dbserver_config['ip_address'];
+				$mysql_db_remote_access = 'y';
+				$mysql_db_remote_ips = $dbserver_config['ip_address'];
+			} else {
+				$mysql_db_server_id = $websrv['server_id'];
+				$mysql_db_host = 'localhost';
+				$mysql_db_remote_access = 'n';
+				$mysql_db_remote_ips = '';
+			}
+			
+			//* Find a free db name for the app
+			for($n = 1; $n <= 1000; $n++) {
+				$mysql_db_name = $dbname_prefix.'aps'.$n;
+				$mysql_db_user = $dbuser_prefix.'aps'.$n;
+				$tmp = $app->db->queryOneRecord("SELECT count(database_id) as number FROM web_database WHERE database_name = '".$app->db->quote($mysql_db_user)."' OR database_user = '".$app->db->quote($mysql_db_name)."'");
+				if($tmp['number'] == 0) break;
+			}
+			
+			//* Create the mysql database
+			$insert_data = "(`sys_userid`, `sys_groupid`, `sys_perm_user`, `sys_perm_group`, `sys_perm_other`, `server_id`, `parent_domain_id`, `type`, `database_name`, `database_user`, `database_password`, `database_charset`, `remote_access`, `remote_ips`, `backup_copies`, `active`, `backup_interval`) 
+					  VALUES( ".$websrv['sys_userid'].", ".$websrv['sys_groupid'].", 'riud', '".$websrv['sys_perm_group']."', '', $mysql_db_server_id, ".$websrv['domain_id'].", 'mysql', '$mysql_db_name', '$mysql_db_user', '$mysql_db_password', '', '$mysql_db_remote_access', '$mysql_db_remote_ips', ".$websrv['backup_copies'].", 'y', '".$websrv['backup_interval']."')";
+			$app->db->datalogInsert('web_database', $insert_data, 'database_id');
+			
+			//* Add db details to package settings
+			$settings['main_database_host'] = $mysql_db_host;
+			$settings['main_database_name'] = $mysql_db_name;
+			$settings['main_database_login'] = $mysql_db_user;
+		
+		}
+		
+		//* Insert new package instance
+		$insert_data = "(`sys_userid`, `sys_groupid`, `sys_perm_user`, `sys_perm_group`, `sys_perm_other`, `server_id`, `customer_id`, `package_id`, `instance_status`) VALUES (".$websrv['sys_userid'].", ".$websrv['sys_groupid'].", 'riud', '".$websrv['sys_perm_group']."', '', ".$this->db->quote($webserver_id).",".$this->db->quote($customerid).", ".$this->db->quote($packageid).", ".INSTANCE_PENDING.")";
+		$InstanceID = $app->db->datalogInsert('aps_instances', $insert_data, 'id');
+		
+		//* Insert all package settings
+		if(is_array($settings)) {
+			foreach($settings as $key => $value) {
+				$insert_data = "(server_id, instance_id, name, value) VALUES (".$this->db->quote($webserver_id).",".$this->db->quote($InstanceID).", '".$this->db->quote($key)."', '".$this->db->quote($value)."')";
+				$this->db->datalogInsert('aps_instances_settings', $insert_data, 'id');
+			}
+		}
+		
+		//* Set package status to install afetr we inserted the settings
+		$app->db->datalogUpdate('aps_instances', "instance_status = ".INSTANCE_INSTALL, 'id', $InstanceID);
+    }
+    
+    /**
+     * Sets the status of an instance to "should be removed" and creates a 
+     * datalog entry to give the ISPConfig server a real removal advice 
+     * 
+     * @param $instanceid the instance to delete
+     */
+    public function deleteInstance($instanceid)
+    {
+        /*
+		$this->db->query("UPDATE aps_instances SET instance_status = ".INSTANCE_REMOVE." WHERE id = ".$instanceid.";");
+        
+        $webserver_id = $this->getInstanceDataForDatalog($instanceid);
+        if($webserver_id == '') return;
+        
+        // Create a sys_datalog entry for deletion
+        $datalog = array('Instance_id' => $instanceid, 'server_id' => $webserver_id);
+        $this->db->datalogSave('aps', 'DELETE', 'id', $instanceid, array(), $datalog);
+		*/
+		$this->db->datalogUpdate('aps_instances', "instance_status = ".INSTANCE_REMOVE, 'id', $instanceid);
+    }
+    
+    /**
+     * Sets the status of an instance to "installation planned" and creates a 
+     * datalog entry to re-install the package. The existing package is simply overwritten. 
+     * 
+     * @param $instanceid the instance to delete
+     */
+    public function reinstallInstance($instanceid)
+    {
+        /*
+		$this->db->query("UPDATE aps_instances SET instance_status = ".INSTANCE_INSTALL." WHERE id = ".$instanceid.";");
+        
+        $webserver_id = $this->getInstanceDataForDatalog($instanceid);
+        if($webserver_id == '') return;
+        
+        // Create a sys_datalog entry for re-installation
+        $datalog = array('instance_id' => $instanceid, 'server_id' => $webserver_id);
+        $this->db->datalogSave('aps', 'INSERT', 'id', $instanceid, array(), $datalog);
+		*/
+		$this->db->datalogUpdate('aps_instances', "instance_status = ".INSTANCE_INSTALL, 'id', $instanceid);
+    }
+
+    /**
+     * Read the settings to be filled when installing
+     * 
+     * @param $id the internal ID of the package
+     * @return array
+     */
+    public function getPackageSettings($id)    
+    {
+        $pkg = $this->db->queryOneRecord('SELECT * FROM aps_packages WHERE id = '.$this->db->quote($id).';');
+        
+        // Load in meta file if existing and register its namespaces
+        $metafile = $this->interface_pkg_dir.'/'.$pkg['path'].'/APP-META.xml';
+        if(!file_exists($metafile)) 
+            return array('error' => 'The metafile for '.$settings['Name'].' couldn\'t be found');
+        
+        $sxe = $this->readInMetaFile($metafile);
+        
+        $groupsettings = parent::getXPathValue($sxe, '//settings/group/setting', true);
+        if(empty($groupsettings)) return array();
+        
+        $settings = array();
+        foreach($groupsettings as $setting)
+        {
+            $setting_id = strval($setting['id']);
+            
+            if($setting['type'] == 'string' || $setting['type'] == 'email' || $setting['type'] == 'integer'
+            || $setting['type'] == 'float' || $setting['type'] == 'domain-name')
+            {
+                $settings[] = array('SettingID' => $setting_id,
+                                    'SettingName' => $setting->name,
+                                    'SettingDescription' => $setting->description,
+                                    'SettingType' => $setting['type'],
+                                    'SettingInputType' => 'string',
+                                    'SettingDefaultValue' => strval($setting['default-value']),
+                                    'SettingRegex' => $setting['regex'],
+                                    'SettingMinLength' => $setting['min-length'],
+                                    'SettingMaxLength' => $setting['max-length']);
+            }
+            else if($setting['type'] == 'password')
+            {
+                $settings[] = array('SettingID' => $setting_id,
+                                    'SettingName' => $setting->name,
+                                    'SettingDescription' => $setting->description,
+                                    'SettingType' => 'password',
+                                    'SettingInputType' => 'password',
+                                    'SettingDefaultValue' => '',
+                                    'SettingRegex' => $setting['regex'],
+                                    'SettingMinLength' => $setting['min-length'],
+                                    'SettingMaxLength' => $setting['max-length']);
+            }
+            else if($setting['type'] == 'boolean')
+            {
+                $settings[] = array('SettingID' => $setting_id,
+                                    'SettingName' => $setting->name,
+                                    'SettingDescription' => $setting->description,
+                                    'SettingType' => 'boolean',
+                                    'SettingInputType' => 'checkbox',
+                                    'SettingDefaultValue' => strval($setting['default-value']));
+            }
+            else if($setting['type'] == 'enum')
+            {
+              $choices = array();
+              foreach($setting->choice as $choice)
+              {
+                $choices[] = array('EnumID' => strval($choice['id']),
+                                   'EnumName' => $choice->name);
+              }
+              $settings[] = array('SettingID' => $setting_id,
+                                  'SettingName' => $setting->name,
+                                  'SettingDescription' => $setting->description,
+                                  'SettingType' => 'enum',
+                                  'SettingInputType' => 'select',
+                                  'SettingDefaultValue' => strval($setting['default-value']),
+                                  'SettingChoices' => $choices);
+            }
+        }
+
+        return $settings;
+    }
+    
+    /**
+     * Validates the user input according to the settings array and
+     * delivers errors if occurring
+     * 
+     * @param $input the user $_POST array
+     * @param $pkg_details the package details
+     * @param $settings the package settings array
+     * @return array in this structure:
+     *               array(2) {
+     *                  ["input"]=> ...
+     *                  ["errors"]=> ...
+     *               }
+     */
+    public function validateInstallerInput($postinput, $pkg_details, $domains, $settings = array())
+    {
+        $ret = array();
+        $input = array(); 
+        $error = array();
+        
+        // Main domain (obligatory)
+        if(isset($postinput['main_domain']))
+        {
+            if(!in_array($postinput['main_domain'], $domains)) $error[] = $this->app->lng('error_main_domain');
+            else $input['main_domain'] = $postinput['main_domain'];
+        }
+        else $error[] = $this->app->lng('error_main_domain'); 
+        
+        // Main location (not obligatory but must be supplied)
+        if(isset($postinput['main_location']))
+        {
+            $temp_errstr = '';
+            // It can be empty but if the user did write something, check it
+            $userinput = false;
+            if(strlen($postinput['main_location']) > 0) $userinput = true; 
+            
+            // Filter invalid input slashes (twice!)
+            $main_location = $this->secureLocation($postinput['main_location']);
+            $main_location = $this->secureLocation($main_location);
+            // Only allow digits, words, / and -
+            $main_location = preg_replace("/[^\d\w\/\-]/i", "", $main_location);
+            if($userinput && (strlen($main_location) == 0)) $temp_errstr = $this->app->lng('error_inv_main_location');
+            
+            // Find out document_root and make sure no apps are installed twice to one location
+            if(in_array($postinput['main_domain'], $domains))
+            {
+                $docroot = $this->db->queryOneRecord("SELECT document_root FROM web_domain 
+                    WHERE domain = '".$this->db->quote($postinput['main_domain'])."';");
+                $new_path = $docroot['document_root'];
+                if(substr($new_path, -1) != '/') $new_path .= '/';
+                $new_path .= $main_location;
+                
+                // Get the $customerid which belongs to the selected domain
+                $customerid = $this->getCustomerIDFromDomain($postinput['main_domain']);
+                
+                // First get all domains used for an install, then their loop them
+                // and get the corresponding document roots as well as the defined
+                // locations. If an existing doc_root + location matches with the
+                // new one -> error
+                $instance_domains = $this->db->queryAllRecords("SELECT instance_id, s.value AS domain 
+                    FROM aps_instances AS i, aps_instances_settings AS s 
+                    WHERE i.id = s.instance_id AND s.name = 'main_domain' 
+                        AND i.customer_id = '".$this->db->quote($customerid)."';");
+                for($i = 0; $i < count($instance_domains); $i++)
+                {
+                    $used_path = '';
+                    
+                    $doc_root = $this->db->queryOneRecord("SELECT document_root FROM web_domain 
+                        WHERE domain = '".$this->db->quote($instance_domains[$i]['domain'])."';");
+
+                    // Probably the domain settings were changed later, so make sure the doc_root
+                    // is not empty for further validation
+                    if(!empty($doc_root))
+                    {
+                        $used_path = $docroot['document_root'];
+                        if(substr($used_path, -1) != '/') $used_path .= '/';
+                        
+                        $location_for_domain = $this->db->queryOneRecord("SELECT value 
+                            FROM aps_instances_settings WHERE name = 'main_location' 
+                            AND instance_id = '".$this->db->quote($instance_domains[$i]['instance_id'])."';");
+                        
+                        // The location might be empty but the DB return must not be false!
+                        if($location_for_domain) $used_path .= $location_for_domain['value'];      
+
+                        if($new_path == $used_path)
+                        {
+                            $temp_errstr = $this->app->lng('error_used_location');
+                            break;
+                        }
+                    }
+                }
+            }
+            else $temp_errstr = $this->app->lng('error_main_domain');
+            
+            if($temp_errstr == '') $input['main_location'] = htmlspecialchars($main_location);
+            else $error[] = $temp_errstr;            
+        }
+        else $error[] = $this->app->lng('error_no_main_location');
+        
+        // License (the checkbox must be set)
+        if(isset($pkg_details['License need agree']) 
+        && $pkg_details['License need agree'] == 'true')
+        {
+            if(isset($postinput['license']) && $postinput['license'] == 'on') $input['license'] = 'true';
+            else $error[] = $this->app->lng('error_license_agreement');
+        } 
+        
+        // Database
+        if(isset($pkg_details['Requirements Database'])
+        && $pkg_details['Requirements Database'] != '')
+        {
+            if(isset($postinput['main_database_password']))
+            {
+                if($postinput['main_database_password'] == '') $error[] = $this->app->lng('error_no_database_pw');
+                else if(strlen($postinput['main_database_password']) > 8) 
+                    $input['main_database_password'] = htmlspecialchars($postinput['main_database_password']);
+                else $error[] = $this->app->lng('error_short_database_pw');
+            }
+            else $error[] = $this->app->lng('error_no_database_pw');
+        }
+        
+        // Validate the package settings 
+        foreach($settings as $setting)
+        {
+            $temp_errstr = '';
+            $setting_id = strval($setting['SettingID']); 
+            
+            // We assume that every setting must be set
+            if((isset($postinput[$setting_id]) && ($postinput[$setting_id] != ''))
+            || ($setting['SettingType'] == 'boolean'))
+            {
+                if($setting['SettingType'] == 'string' || $setting['SettingType'] == 'password')
+                {
+                    if(intval($setting['SettingMinLength']) != 0 
+                    && strlen($postinput[$setting_id]) < intval($setting['SettingMinLength']))
+                        $temp_errstr = sprintf($this->app->lng('error_short_value_for'), $setting['setting_name']);
+                        
+                    if(intval($setting['SettingMaxLength']) != 0 
+                    && strlen($postinput[$setting_id]) > intval($setting['SettingMaxLength']))
+                        $temp_errstr = sprintf($this->app->lng('error_long_value_for'), $setting['setting_name']);
+
+                    if(isset($setting['SettingRegex'])
+                    && !preg_match("/".$setting['SettingRegex']."/", $postinput[$setting_id]))
+                        $temp_errstr = sprintf($this->app->lng('error_inv_value_for'), $setting['setting_name']);
+                }
+                else if($setting['SettingType'] == 'email')
+                {
+                    if(filter_var(strtolower($postinput[$setting_id]), FILTER_VALIDATE_EMAIL) === false)
+                        $temp_errstr = sprintf($this->app->lng('error_inv_email_for'), $setting['setting_name']);
+                }
+                else if($setting['SettingType'] == 'domain-name')
+                {
+                    if(!preg_match("^(http|https)\://([a-zA-Z0-9\.\-]+(\:[a-zA-Z0-9\.&%\$\-]+)*@)*((25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9])\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0)\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0)\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[0-9])|localhost|([a-zA-Z0-9\-]+\.)*[a-zA-Z0-9\-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(\:[0-9]+)*(/($|[a-zA-Z0-9\.\,\?\'\\\+&%\$#\=~_\-]+))*$", 
+                        $postinput[$setting_id]))
+                    $temp_errstr = sprintf($this->app->lng('error_inv_domain_for'), $setting['setting_name']);    
+                }
+                else if($setting['SettingType'] == 'integer')
+                {
+                    if(filter_var($postinput[$setting_id], FILTER_VALIDATE_INT) === false)
+                        $temp_errstr = sprintf($this->app->lng('error_inv_integer_for'), $setting['setting_name']);
+                }
+                else if($setting['SettingType'] == 'float')
+                {
+                    if(filter_var($postinput[$setting_id], FILTER_VALIDATE_FLOAT) === false)
+                        $temp_errstr = sprintf($this->app->lng('error_inv_float_for'), $setting['setting_name']);
+                }
+                else if($setting['SettingType'] == 'boolean')
+                {
+                    // If we have a boolean value set, it must be either true or false
+                    if(!isset($postinput[$setting_id])) $postinput[$setting_id] = 'false';
+                    else if(isset($postinput[$setting_id]) && $postinput[$setting_id] != 'true') 
+                        $postinput[$setting_id] = 'true';
+                }
+                else if($setting['SettingType'] == 'enum')
+                {
+                    $found = false;
+                    for($i = 0; $i < count($setting['SettingChoices']); $i++)
+                    {
+                        if($setting['SettingChoices'][$i]['EnumID'] == $postinput[$setting_id])
+                            $found = true;
+                    }
+                    if(!$found) $temp_errstr = sprintf($this->app->lng('error_inv_value_for'), $setting['SettingName']);
+                }
+                
+                if($temp_errstr == '') $input[$setting_id] = $postinput[$setting_id];
+                else $error[] = $temp_errstr;
+            }
+            else $error[] = sprintf($this->app->lng('error_no_value_for'), $setting['SettingName']);
+        }
+        
+        $ret['input'] = $input;
+        $ret['error'] = array_unique($error);
+        
+        return $ret;
+    }
+    
+    /**
+     * Read the metadata of a package and returns some content
+     * 
+     * @param $id the internal ID of the package
+     * @return array
+     */
+    public function getPackageDetails($id)
+    {
+        $pkg = $this->db->queryOneRecord('SELECT * FROM aps_packages WHERE id = '.$this->db->quote($id).';');
+        
+        // Load in meta file if existing and register its namespaces
+        $metafile = $this->interface_pkg_dir.'/'.$pkg['path'].'/APP-META.xml';
+        if(!file_exists($metafile)) 
+            return array('error' => 'The metafile for '.$pkg['name'].' couldn\'t be found');
+        
+        $metadata = file_get_contents($metafile);
+        $metadata = str_replace("xmlns=", "ns=", $metadata);
+        $sxe = new SimpleXMLElement($metadata);
+        $namespaces = $sxe->getDocNamespaces(true);
+        foreach($namespaces as $ns => $url) $sxe->registerXPathNamespace($ns, $url); 
+
+        $pkg['Summary'] = htmlspecialchars(parent::getXPathValue($sxe, '//summary'));
+        $pkg['Homepage'] = parent::getXPathValue($sxe, '//homepage');
+        $pkg['Description'] = nl2br(htmlspecialchars(trim(parent::getXPathValue($sxe, '//description'))));
+        $pkg['Config script'] = strtoupper(parent::getXPathValue($sxe, '//configuration-script-language'));
+        $installed_size = parent::getXPathValue($sxe, '//installed-size');
+        $pkg['Installed Size'] = (!empty($installed_size)) ? parent::convertSize((int)$installed_size) : '';        
+        
+        // License
+        $pkg['License need agree'] = parent::getXPathValue($sxe, '//license/@must-accept');
+        $pkg['License name'] = parent::getXPathValue($sxe, '//license/text/name'); // might be empty
+        $pkg['License type'] = 'file'; // default type
+        $pkg['License content'] = ''; // default license filename on local system
+        $license_url = parent::getXPathValue($sxe, '//license/text/url');
+        if(!empty($license_url)) 
+        {
+            $pkg['License type'] = 'url';
+            $pkg['License content'] = htmlspecialchars($license_url);
+        }
+        else
+        {
+            $lic = @file_get_contents($this->interface_pkg_dir.'/'.$pkg['path'].'/LICENSE');
+            $pkg['License content'] = htmlentities($lic, ENT_QUOTES, 'ISO-8859-1');
+        }  
+        
+        // Languages
+        $languages = parent::getXPathValue($sxe, '//languages/language', true);
+        $pkg['Languages'] = (is_array($languages)) ? implode(' ', $languages) : '';
+        
+        // Icon
+        $icon = parent::getXPathValue($sxe, '//icon/@path');
+        if(!empty($icon))
+        {
+            // Using parse_url() to filter malformed URLs
+            $path = dirname(parse_url($_SERVER['PHP_SELF'], PHP_URL_PATH)).'/'.
+                    basename($this->interface_pkg_dir).'/'.$pkg['path'].'/'.basename((string)$icon);
+            $pkg['Icon'] = $path;
+        }
+        else $pkg['Icon'] = '';
+        
+        // Screenshots
+        $screenshots = parent::getXPathValue($sxe, '//screenshot', true);
+        if(!empty($screenshots))
+        {
+            foreach($screenshots as $screen)
+            {
+                // Using parse_url() to filter malformed URLs
+                $path = dirname(parse_url($_SERVER['PHP_SELF'], PHP_URL_PATH)).'/'.
+                        basename($this->interface_pkg_dir).'/'.$pkg['path'].'/'.basename((string)$screen['path']);
+
+                $pkg['Screenshots'][] = array('ScreenPath' => $path,
+                                              'ScreenDescription' => htmlspecialchars(trim((string)$screen->description)));
+            }
+        }
+        else $pkg['Screenshots'] = ''; // if no screenshots are available, set the variable though
+                
+        // Changelog
+        $changelog = parent::getXPathValue($sxe, '//changelog/version', true);
+        if(!empty($changelog))
+        {
+            foreach($changelog as $change)
+            {
+                $entries = array(); 
+                foreach($change->entry as $entry) $entries[] = htmlspecialchars(trim((string)$entry)); 
+
+                $pkg['Changelog'][] = array('ChangelogVersion' => (string)$change['version'], 
+                                            'ChangelogDescription' => implode('<br />', $entries));
+            }
+        }
+		
+        else $pkg['Changelog'] = '';
+        
+        // PHP extensions
+        $php_extensions = parent::getXPathValue($sxe, '//php:extension', true);
+        $php_ext = '';
+        if(!empty($php_extensions))
+        {
+            foreach($php_extensions as $extension)
+            {
+                if(strtolower($extension) == 'php') continue;
+                $php_ext .= $extension.' ';
+            }
+        }
+        $pkg['Requirements PHP extensions'] = trim($php_ext);
+        
+        // PHP bool options
+        $pkg['Requirements PHP settings'] = '';
+        $php_bool_options = array('allow-url-fopen', 'file-uploads', 'magic-quotes-gpc', 
+                                  'register-globals', 'safe-mode', 'short-open-tag');
+        foreach($php_bool_options as $option)
+        {
+            $value = parent::getXPathValue($sxe, '//php:'.$option);
+            if(!empty($value))
+            {
+                $option = str_replace('-', '_', $option);
+                $value = str_replace(array('false', 'true'), array('off', 'on'), $value);
+                $pkg['Requirements PHP settings'][] = array('PHPSettingName' => $option,
+                                                            'PHPSettingValue' => $value);
+            }
+        }
+        
+        // PHP integer value settings
+        $memory_limit = parent::getXPathValue($sxe, '//php:memory-limit');
+        if(!empty($memory_limit))
+            $pkg['Requirements PHP settings'][] = array('PHPSettingName' => 'memory_limit',
+                                                        'PHPSettingValue' => parent::convertSize((int)$memory_limit));
+
+        $max_exec_time = parent::getXPathValue($sxe, '//php:max-execution-time');
+        if(!empty($max_exec_time))
+            $pkg['Requirements PHP settings'][] = array('PHPSettingName' => 'max-execution-time',
+                                                        'PHPSettingValue' => $max_exec_time);
+        
+        $post_max_size = parent::getXPathValue($sxe, '//php:post-max-size');
+        if(!empty($post_max_size))
+            $pkg['Requirements PHP settings'][] = array('PHPSettingName' => 'post_max_size',
+                                                        'PHPSettingValue' => parent::convertSize((int)$post_max_size));
+        
+        // Get supported PHP versions
+        $pkg['Requirements Supported PHP versions'] = '';
+        $php_min_version = parent::getXPathValue($sxe, '//php:version/@min');
+        $php_max_not_including = parent::getXPathValue($sxe, '//php:version/@max-not-including');
+        if(!empty($php_min_version) && !empty($php_max_not_including)) 
+            $pkg['Requirements Supported PHP versions'] = $php_min_version.' - '.$php_max_not_including;
+        else if(!empty($php_min_version)) 
+            $pkg['Requirements Supported PHP versions'] = '> '.$php_min_version;
+        else if(!empty($php_max_not_including))
+            $pkg['Requirements Supported PHP versions'] = '< '.$php_min_version;
+        
+        // Database
+        $db_id = parent::getXPathValue($sxe, '//db:id');
+        $db_server_type = parent::getXPathValue($sxe, '//db:server-type');
+        $db_min_version = parent::getXPathValue($sxe, '//db:server-min-version'); 
+        if(!empty($db_id))
+        {
+            $db_server_type = str_replace('postgresql', 'PostgreSQL', $db_server_type);
+            $db_server_type = str_replace('microsoft:sqlserver', 'MSSQL', $db_server_type);
+            $db_server_type = str_replace('mysql', 'MySQL', $db_server_type);
+            
+            $pkg['Requirements Database'] = $db_server_type;
+            if(!empty($db_min_version)) $pkg['Requirements Database'] .= ' > '.$db_min_version;
+        }
+        else $pkg['Requirements Database'] = '';
+        
+        return $pkg;
+    }
+}
+?>
\ No newline at end of file
diff --git a/interface/web/js/scrigo.js.php b/interface/web/js/scrigo.js.php
index 4a33f96..75200ab 100644
--- a/interface/web/js/scrigo.js.php
+++ b/interface/web/js/scrigo.js.php
@@ -203,6 +203,9 @@
   var pageContentObject2 = jQuery.ajax({	type: "GET", 
 											url: pagename,
 											dataType: "html",
+											beforeSend: function() {
+												jQuery('#pageContent').html('<div id="ajaxloader"><img src="themes/default/images/ajax-loader.gif" /></div>');
+											},
 											success: function(data, textStatus, jqXHR) {
 												if(jqXHR.responseText.indexOf('HEADER_REDIRECT:') > -1) {
 													var parts = jqXHR.responseText.split(':');
@@ -215,9 +218,9 @@
 													//var reponse = jQuery(jqXHR.responseText);
 													//var reponseScript = reponse.filter("script");
 													//jQuery.each(reponseScript, function(idx, val) { eval(val.text); } );
+													
 													jQuery('#pageContent').html(jqXHR.responseText);
 												}
-												
 											},
 											error: function() {
 												reportError('Ajax Request was not successful. 113');
diff --git a/interface/web/sites/aps_availablepackages_list.php b/interface/web/sites/aps_availablepackages_list.php
new file mode 100644
index 0000000..3e3b83b
--- /dev/null
+++ b/interface/web/sites/aps_availablepackages_list.php
@@ -0,0 +1,56 @@
+<?php
+/*
+Copyright (c) 2012, ISPConfig UG
+Contributors: web wack creations,  http://www.web-wack.at
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice,
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice,
+      this list of conditions and the following disclaimer in the documentation
+      and/or other materials provided with the distribution.
+    * Neither the name of ISPConfig nor the names of its contributors
+      may be used to endorse or promote products derived from this software without
+      specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+require_once('../../lib/config.inc.php');
+require_once('../../lib/app.inc.php');
+//require_once('classes/class.base.php'); // for constants
+$app->load('aps_base');
+
+// Path to the list definition file
+$list_def_file = "list/aps_availablepackages.list.php";
+
+// Check the module permissions
+$app->auth->check_module_permissions('sites');
+        
+// Load needed classes
+$app->uses('tpl,listform_actions');
+
+$app->listform_actions->SQLOrderBy = 'ORDER BY name, version';
+// Show only unlocked packages to clients and (un-)lockable packages to admins
+if($_SESSION['s']['user']['typ'] != 'admin') $app->listform_actions->SQLExtWhere = 'package_status = '.PACKAGE_ENABLED;
+else $app->listform_actions->SQLExtWhere = '(package_status = '.PACKAGE_ENABLED.' OR package_status = '.PACKAGE_LOCKED.')';
+
+// Get package amount
+$pkg_count = $app->db->queryOneRecord("SELECT COUNT(*) FROM aps_packages");
+$app->tpl->setVar("package_count", $pkg_count['COUNT(*)']);
+
+// Start the form rendering and action handling
+$app->listform_actions->onLoad();
+?>
\ No newline at end of file
diff --git a/interface/web/sites/aps_cron_apscrawler_if.php b/interface/web/sites/aps_cron_apscrawler_if.php
new file mode 100644
index 0000000..e40b746
--- /dev/null
+++ b/interface/web/sites/aps_cron_apscrawler_if.php
@@ -0,0 +1,59 @@
+<?php
+/*
+Copyright (c) 2012, ISPConfig UG
+Contributors: web wack creations,  http://www.web-wack.at
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice,
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice,
+      this list of conditions and the following disclaimer in the documentation
+      and/or other materials provided with the distribution.
+    * Neither the name of ISPConfig nor the names of its contributors
+      may be used to endorse or promote products derived from this software without
+      specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+require_once('../../lib/config.inc.php');
+require_once('../../lib/app.inc.php'); 
+//require_once('classes/class.crawler.php');
+$app->load('aps_crawler');
+
+$log_prefix = 'APS crawler cron: ';
+
+$aps = new ApsCrawler($app, true); // true = Interface mode, false = Server mode
+
+$app->log($log_prefix.'Used mem at begin: '.$aps->convertSize(memory_get_usage(true)));
+
+$time_start = microtime(true);
+$aps->startCrawler();
+$aps->parseFolderToDB();
+$time = microtime(true) - $time_start;
+
+$app->log($log_prefix.'Used mem at end: '.$aps->convertSize(memory_get_usage(true)));
+$app->log($log_prefix.'Mem peak during execution: '.$aps->convertSize(memory_get_peak_usage(true)));
+$app->log($log_prefix.'Execution time: '.round($time, 3).' seconds');
+
+// Load the language file
+$lngfile = 'lib/lang/'.$_SESSION['s']['language'].'_aps.lng';
+require_once($lngfile);
+$app->load_language_file('web/sites/'.$lngfile);
+
+echo '<div id="OKMsg"><p>'.$app->lng('packagelist_update_finished_txt').'</p></div>';
+
+
+
+?>
\ No newline at end of file
diff --git a/interface/web/sites/aps_do_operation.php b/interface/web/sites/aps_do_operation.php
new file mode 100644
index 0000000..493cde4
--- /dev/null
+++ b/interface/web/sites/aps_do_operation.php
@@ -0,0 +1,110 @@
+<?php
+/*
+Copyright (c) 2012, ISPConfig UG
+Contributors: web wack creations,  http://www.web-wack.at
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice,
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice,
+      this list of conditions and the following disclaimer in the documentation
+      and/or other materials provided with the distribution.
+    * Neither the name of ISPConfig nor the names of its contributors
+      may be used to endorse or promote products derived from this software without
+      specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+ 
+require_once('../../lib/config.inc.php');
+require_once('../../lib/app.inc.php');
+require_once('classes/class.guicontroller.php');
+
+// Check the module permissions
+$app->auth->check_module_permissions('aps');
+
+$gui = new ApsGUIController($app);
+
+// An action and ID are required in any case
+if(!isset($_GET['action'])) die;
+
+// List of operations which can be performed
+if($_GET['action'] == 'change_status')
+{
+    // Only admins can perform this operation
+    if($_SESSION['s']['user']['typ'] != 'admin') die;
+    
+    // Make sure a valid package ID is given
+    if(!$gui->isValidPackageID($_GET['id'], true)) die($app->lng('Invalid ID'));
+    
+    // Change the existing status to the opposite
+    $get_status = $app->db->queryOneRecord("SELECT PackageStatus FROM aps_packages WHERE ID = '".intval($_GET['id'])."';");
+    if($get_status['PackageStatus'] == strval(PACKAGE_LOCKED))
+    {
+        $app->db->query("UPDATE aps_packages SET PackageStatus = ".PACKAGE_ENABLED." WHERE ID = '".intval($_GET['id'])."';");
+        echo '<div class="swap" id="ir-Yes"><span>'.$app->lng('Yes').'</span></div>';
+    }
+    else
+    {
+        $app->db->query("UPDATE aps_packages SET PackageStatus = ".PACKAGE_LOCKED." WHERE ID = '".intval($_GET['id'])."';");
+        echo '<div class="swap" id="ir-No"><span>'.$app->lng('No').'</span></div>';
+    }
+}
+else if($_GET['action'] == 'delete_instance')
+{
+    // Make sure a valid package ID is given (also corresponding to the calling user)
+    $client_id = 0;
+    $is_admin = ($_SESSION['s']['user']['typ'] == 'admin') ? true : false;
+    if(!$is_admin)
+    {
+        $cid = $app->db->queryOneRecord("SELECT client_id FROM client WHERE username = '".$app->db->quote($_SESSION['s']['user']['username'])."';");
+        $client_id = $cid['client_id'];
+    }
+    // Assume that the given instance belongs to the currently calling client_id. Unimportant if status is admin
+    if(!$gui->isValidInstanceID($_GET['id'], $client_id, $is_admin)) die($app->lng('Invalid ID'));
+    
+    // Only delete the instance if the status is "installed" or "flawed"
+    $check = $app->db->queryOneRecord("SELECT ID FROM aps_instances 
+        WHERE ID = ".$app->db->quote($_GET['id'])." AND 
+        (InstanceStatus = ".INSTANCE_SUCCESS." OR InstanceStatus = ".INSTANCE_ERROR.");");
+    if(!empty($check)) $gui->deleteInstance($_GET['id']);
+    
+    echo $app->lng('Installation_remove');
+}
+else if($_GET['action'] == 'reinstall_instance')
+{
+    // Make sure a valid package ID is given (also corresponding to the calling user)
+    $client_id = 0;
+    $is_admin = ($_SESSION['s']['user']['typ'] == 'admin') ? true : false;
+    if(!$is_admin)
+    {
+        $cid = $app->db->queryOneRecord("SELECT client_id FROM client WHERE username = '".$app->db->quote($_SESSION['s']['user']['username'])."';");
+        $client_id = $cid['client_id'];
+    }
+    // Assume that the given instance belongs to the currently calling client_id. Unimportant if status is admin
+    if(!$gui->isValidInstanceID($_GET['id'], $client_id, $is_admin)) die($app->lng('Invalid ID'));
+    
+    // We've an InstanceID, so make sure the package is no enabled and InstanceStatus is still "installed"
+    $check = $app->db->queryOneRecord("SELECT aps_instances.ID FROM aps_instances, aps_packages 
+        WHERE aps_instances.PackageID = aps_packages.ID 
+        AND aps_instances.InstanceStatus = ".INSTANCE_SUCCESS." 
+        AND aps_packages.PackageStatus = ".PACKAGE_ENABLED." 
+        AND aps_instances.ID = ".$app->db->quote($_GET['id']).";");
+    if(!$check) die; // normally this might not happen at all, so just die
+    
+    $gui->reinstallInstance($_GET['id']);
+    echo $app->lng('Installation_task');
+}
+?>
diff --git a/interface/web/sites/aps_install_package.php b/interface/web/sites/aps_install_package.php
new file mode 100644
index 0000000..be60121
--- /dev/null
+++ b/interface/web/sites/aps_install_package.php
@@ -0,0 +1,198 @@
+<?php
+/*
+Copyright (c) 2012, ISPConfig UG
+Contributors: web wack creations,  http://www.web-wack.at
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice,
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice,
+      this list of conditions and the following disclaimer in the documentation
+      and/or other materials provided with the distribution.
+    * Neither the name of ISPConfig nor the names of its contributors
+      may be used to endorse or promote products derived from this software without
+      specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+require_once('../../lib/config.inc.php');
+require_once('../../lib/app.inc.php');
+//require_once('classes/class.guicontroller.php');
+$app->load('aps_guicontroller');
+
+// Check the module permissions
+$app->auth->check_module_permissions('sites');
+
+// Load needed classes
+$app->uses('tpl');
+$app->tpl->newTemplate("form.tpl.htm");
+$app->tpl->setInclude('content_tpl', 'templates/aps_install_package.htm');
+
+// Load the language file
+$lngfile = 'lib/lang/'.$_SESSION['s']['language'].'_aps.lng';
+require_once($lngfile);
+$app->tpl->setVar($wb);
+$app->load_language_file('web/sites/'.$lngfile);
+
+$adminflag = ($_SESSION['s']['user']['typ'] == 'admin') ? true : false;
+$gui = new ApsGUIController($app);
+$pkg_id = (isset($_GET['id'])) ? $app->db->quote($_GET['id']) : '';
+
+// Check if a newer version is available for the current package
+// Note: It's intended that here is no strict ID check (see below)
+if(isset($pkg_id))
+{
+    $newest_pkg_id = $gui->getNewestPackageID($pkg_id);
+    if($newest_pkg_id != 0) $pkg_id = $newest_pkg_id;
+}
+
+// Make sure an integer ID is given
+if(!isset($pkg_id) || !$gui->isValidPackageID($pkg_id, $adminflag))
+    $app->error($app->lng('Invalid ID'));
+
+// Get package details
+$details = $gui->getPackageDetails($pkg_id);
+if(isset($details['error'])) $app->error($details['error']);
+$settings = $gui->getPackageSettings($pkg_id);
+if(isset($settings['error'])) $app->error($settings['error']);
+
+// Get domain list
+$domains = array();
+$domain_for_user = '';
+if(!$adminflag) $domain_for_user = "AND (sys_userid = '".$app->db->quote($_SESSION['s']['user']['userid'])."' 
+    OR sys_groupid = '".$app->db->quote($_SESSION['s']['user']['userid'])."' )";
+$domains_assoc = $app->db->queryAllRecords("SELECT domain FROM web_domain WHERE document_root != '' ".$domain_for_user." ORDER BY domain;");
+if(!empty($domains_assoc)) foreach($domains_assoc as $domain) $domains[] = $domain['domain'];
+
+// If data has been submitted, validate it
+$result['input'] = array();
+if(count($_POST) > 1)
+{
+    $result = $gui->validateInstallerInput($_POST, $details, $domains, $settings);
+    if(empty($result['error']))
+    {
+        $gui->createPackageInstance($result['input'], $pkg_id);
+        @header('Location:aps_installedpackages_list.php');
+    }
+    else
+    {
+        $app->tpl->setVar('error', implode('<br />', $result['error']));
+        
+        // Set memorized values (license, db password, install location)
+        if(!empty($result['input']))
+            foreach($result['input'] as $key => $value) $app->tpl->setVar('inp_'.$key, $value);
+    }
+}
+else $app->tpl->setVar('inp_main_database_password', ucfirst(substr(md5(crypt(rand(0, 10))), 0, 16)));
+
+// Pass the package details to the template
+foreach($details as $key => $value)
+{
+    if(!is_array($value)) $app->tpl->setVar('pkg_'.str_replace(' ', '_', strtolower($key)), $value);
+    else if($key == 'Requirements PHP settings') $app->tpl->setLoop('pkg_requirements_php_settings', $details['Requirements PHP settings']);
+}
+
+// Parse the template as far as possible, then do the rest manually 
+$app->tpl_defaults();
+$parsed_tpl = $app->tpl->grab();
+
+
+// ISPConfig has a very old and functionally limited template engine. We have to style parts on our own...
+
+// Print the domain list
+$domains_tpl = '';
+if(!empty($domains))
+{
+    $set = array();
+    $set[] = '<select name="main_domain" id="main_domain" class="selectInput">';
+    foreach($domains as $domain)
+    {
+        $selected = '';
+        if((count($_POST) > 1)
+        && (isset($result['input']['main_domain']))
+        && ($result['input']['main_domain'] == $domain))
+            $selected = ' selected ';
+        $set[] = '<option value="'.$domain.'" '.$selected.'>'.$domain.'</option>';
+    }
+    $set[] = '</select>';
+    
+    $domains_tpl = implode("\n", $set);
+}
+$parsed_tpl = str_replace('DOMAIN_LIST_SPACE', $domains_tpl, $parsed_tpl);
+
+// Print the packgae settings
+$settings_tpl = '';
+if(!empty($settings))
+{
+    $set = array();
+    $set[] = '<legend>'.$app->lng('package_settings_txt').'</legend>';
+    foreach($settings as $setting)
+    {
+        $set[] = '<div class="ctrlHolder">';
+        $set[] = '<label for="'.$setting['SettingID'].'">'.$setting['SettingName'].'</label>';
+        if($setting['SettingInputType'] == 'string' || $setting['SettingInputType'] == 'password')
+        {
+            $input_type = ($setting['SettingInputType'] == 'string') ? 'text' : 'password';
+              
+            $input_value = '';
+            if((count($_POST) > 1) 
+            && (isset($result['input'][$setting['SettingID']]))) 
+                $input_value = $result['input'][$setting['SettingID']];
+            else $input_value = @$setting['SettingDefaultValue'];
+            
+            $set[] = '<input type="'.$input_type.'" class="textInput" name="'.$setting['SettingID'].'" maxlength="'.$setting['SettingMaxLength'].'" id="'.$setting['SettingID'].'" value="'.$input_value.'" />
+                <p class="formHint">'.$setting['SettingDescription'].'</p>';
+        }
+        else if($setting['SettingInputType'] == 'checkbox')
+        {
+            $checked = '';
+            if((count($_POST) > 1) 
+            && (isset($result['input'][$setting['SettingID']]) 
+            && ($result['input'][$setting['SettingID']] == 'true'))) 
+                $checked = 'checked ';
+            else if($setting['SettingDefaultValue'] == '1') $checked = 'checked ';
+            
+            $set[] = '<input type="checkbox" id="'.$setting['SettingID'].'" name="'.$setting['SettingID'].'" '.$checked.'/>
+                <p class="formHint">'.$setting['SettingDescription'].'</p>';
+        }
+        else if($setting['SettingInputType'] == 'select')
+        {
+            $set[] =  '<select size="1" class="selectInput" name="'.$setting['SettingID'].'">';
+            foreach($setting['SettingChoices'] as $choice)
+            {
+                $selected = '';
+                if((count($_POST) > 1)
+                && (isset($result['input'][$setting['SettingID']])))
+                { 
+                    if($result['input'][$setting['SettingID']] == $choice['EnumID'])
+                        $selected = 'selected ';
+                }
+                else if($setting['SettingDefaultValue'] == $choice['EnumID']) $selected = 'selected ';
+                
+                $set[] = '<option value="'.$choice['EnumID'].'" '.$selected.'>'.$choice['EnumName'].'</option>';
+            }
+            $set[] = '</select>
+                <p class="formHint">'.$setting['SettingDescription'].'</p>';
+        }
+        
+        $set[] = '</div>';
+    }
+    $settings_tpl = implode("\n", $set);
+}
+$parsed_tpl = str_replace('PKG_SETTINGS_SPACE', $settings_tpl, $parsed_tpl);
+
+echo $parsed_tpl;
+?>
\ No newline at end of file
diff --git a/interface/web/sites/aps_installedpackages_list.php b/interface/web/sites/aps_installedpackages_list.php
new file mode 100644
index 0000000..43b6053
--- /dev/null
+++ b/interface/web/sites/aps_installedpackages_list.php
@@ -0,0 +1,127 @@
+<?php
+/*
+Copyright (c) 2012, ISPConfig UG
+Contributors: web wack creations,  http://www.web-wack.at
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice,
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice,
+      this list of conditions and the following disclaimer in the documentation
+      and/or other materials provided with the distribution.
+    * Neither the name of ISPConfig nor the names of its contributors
+      may be used to endorse or promote products derived from this software without
+      specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+require_once('../../lib/config.inc.php');
+require_once('../../lib/app.inc.php');
+//require_once('classes/class.base.php'); // for constants
+$app->load('aps_base');
+
+// Path to the list definition file
+$list_def_file = "list/aps_installedpackages.list.php";
+
+// Check the module permissions
+$app->auth->check_module_permissions('sites');
+        
+// Load needed classes
+$app->uses('tpl,tform,listform,listform_actions');
+
+// Show further information only to admins or resellers
+if($_SESSION['s']['user']['typ'] == 'admin' || $app->auth->has_clients($_SESSION['s']['user']['userid']))
+    $app->tpl->setVar('is_noclient', 1);
+
+// Show each user the own packages (if not admin)
+$client_ext = '';
+$is_admin = ($_SESSION['s']['user']['typ'] == 'admin') ? true : false;
+if(!$is_admin)
+{
+    $cid = $app->db->queryOneRecord('SELECT client_id FROM client WHERE username = "'.$app->db->quote($_SESSION['s']['user']['username']).'";');
+    $client_ext = ' AND aps_instances.customer_id = '.$cid['client_id'];
+}
+$app->listform_actions->SQLExtWhere = 'aps_instances.package_id = aps_packages.id'.$client_ext;
+$app->listform_actions->SQLOrderBy = 'ORDER BY package_name';
+
+// We are using parts of listform_actions because ISPConfig doesn't allow
+// queries over multiple tables so we construct them ourselves
+$_SESSION['s']['form']['return_to'] = '';
+
+// Load the list template		
+$app->listform->loadListDef($list_def_file);
+if(!is_file('templates/'.$app->listform->listDef["name"].'_list.htm')) 
+{
+$app->uses('listform_tpl_generator');
+$app->listform_tpl_generator->buildHTML($app->listform->listDef);
+}
+$app->tpl->newTemplate("listpage.tpl.htm");
+$app->tpl->setInclude('content_tpl', 'templates/'.$app->listform->listDef["name"].'_list.htm');
+
+// Build the WHERE query for search
+$sql_where = '';
+if($app->listform_actions->SQLExtWhere != '') 
+  $sql_where .= ' '.$app->listform_actions->SQLExtWhere.' and';
+$sql_where = $app->listform->getSearchSQL($sql_where);
+$app->tpl->setVar($app->listform->searchValues);
+		
+// Paging
+$limit_sql = $app->listform->getPagingSQL($sql_where);
+$app->tpl->setVar('paging', $app->listform->pagingHTML);
+
+// Our query over multiple tables
+$query = "SELECT aps_instances.id AS id, aps_instances.package_id AS package_id, 
+                 aps_instances.customer_id AS customer_id, client.username AS customer_name, 
+                 aps_instances.instance_status AS instance_status, aps_packages.name AS package_name, 
+                 aps_packages.version AS package_version, aps_packages.release AS package_release, 
+                 aps_packages.package_status AS package_status, 
+              CONCAT ((SELECT value FROM aps_instances_settings WHERE name='main_domain' AND instance_id = aps_instances.id), 
+                 '/', (SELECT value FROM aps_instances_settings WHERE name='main_location' AND instance_id = aps_instances.id)) 
+                  AS install_location  
+          FROM aps_instances, aps_packages, client 
+          WHERE client.client_id = aps_instances.Customer_id AND ".$sql_where." ".$app->listform_actions->SQLOrderBy." ".$limit_sql;
+
+$records = $app->db->queryAllRecords($query);
+$app->listform_actions->DataRowColor = '#FFFFFF';
+
+// Re-form all result entries and add extra entries 
+$records_new = '';
+if(is_array($records)) 
+{
+    $app->listform_actions->idx_key = $app->listform->listDef["table_idx"]; 
+    foreach($records as $rec)
+    {
+        // Set an abbreviated install location to beware the page layout
+        $ils = '';
+        if(strlen($rec['Install_location']) >= 38) $ils = substr($rec['Install_location'], 0,  35).'...';
+        else $ils = $rec['install_location'];
+        $rec['install_location_short'] = $ils; 
+        
+        // Also set a boolean-like variable for the reinstall button (vlibTemplate doesn't allow variable comparisons)
+        // For a reinstall, the package must be already installed successfully and (still be) enabled
+        if($rec['instance_status'] == INSTANCE_SUCCESS && $rec['package_status'] == PACKAGE_ENABLED) 
+            $rec['reinstall_possible'] = 'true';
+        // Of course an instance can only then be removed when it's not already tagged for removal
+        if($rec['instance_status'] != INSTANCE_REMOVE && $rec['instance_status'] != INSTANCE_INSTALL) 
+            $rec['delete_possible'] = 'true';
+        
+        $records_new[] = $app->listform_actions->prepareDataRow($rec);
+    }
+}
+$app->tpl->setLoop('records', $records_new);
+
+$app->listform_actions->onShow();
+?>
\ No newline at end of file
diff --git a/interface/web/sites/aps_packagedetails_show.php b/interface/web/sites/aps_packagedetails_show.php
new file mode 100644
index 0000000..cff22af
--- /dev/null
+++ b/interface/web/sites/aps_packagedetails_show.php
@@ -0,0 +1,99 @@
+<?php
+/*
+Copyright (c) 2012, ISPConfig UG
+Contributors: web wack creations,  http://www.web-wack.at
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice,
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice,
+      this list of conditions and the following disclaimer in the documentation
+      and/or other materials provided with the distribution.
+    * Neither the name of ISPConfig nor the names of its contributors
+      may be used to endorse or promote products derived from this software without
+      specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+require_once('../../lib/config.inc.php');
+require_once('../../lib/app.inc.php');
+//require_once('classes/class.guicontroller.php');
+$app->load('aps_guicontroller');
+
+// Check the module permissions
+$app->auth->check_module_permissions('sites');
+
+// Load needed classes
+$app->uses('tpl');
+$app->tpl->newTemplate("listpage.tpl.htm");
+$app->tpl->setInclude('content_tpl', 'templates/aps_packagedetails_show.htm');
+
+// Load the language file
+$lngfile = 'lib/lang/'.$_SESSION['s']['language'].'_aps.lng';
+require_once($lngfile);
+$app->tpl->setVar($wb);
+
+$gui = new ApsGUIController($app);
+$pkg_id = (isset($_GET['id'])) ? $app->db->quote($_GET['id']) : '';
+
+// Check if a newer version is available for the current package
+// Note: It's intended that here is no strict ID check (see below)
+if(isset($pkg_id))
+{
+    $newest_pkg_id = $gui->getNewestPackageID($pkg_id);
+    if($newest_pkg_id != 0) $pkg_id = $newest_pkg_id;
+}
+
+// Make sure an integer ID is given
+$adminflag = ($_SESSION['s']['user']['typ'] == 'admin') ? true : false;
+if(!isset($pkg_id) || !$gui->isValidPackageID($pkg_id, $adminflag))
+    $app->error($app->lng('Invalid ID'));
+
+// Get package details
+$details = $gui->getPackageDetails($pkg_id);
+if(isset($details['error'])) $app->error($details['error']);
+
+// Set the active and default tab
+$next_tab = 'details';
+if(isset($_POST['next_tab']))
+{
+    switch($_POST['next_tab'])
+    {
+        case 'details': $next_tab = 'details'; break;
+        case 'settings': $next_tab = 'settings'; break;
+        case 'changelog': $next_tab = 'changelog'; break;
+        case 'screenshots': $next_tab = 'screenshots'; break;
+        default: $next_tab = 'details';
+    }
+}
+$app->tpl->setVar('next_tab', $next_tab);
+
+// Parse the package details to the template
+foreach($details as $key => $value)
+{
+    if(!is_array($value)) $app->tpl->setVar('pkg_'.str_replace(' ', '_', strtolower($key)), $value);
+    else // Special cases
+    {
+        if($key == 'Changelog') $app->tpl->setLoop('pkg_changelog', $details['Changelog']);
+        elseif($key == 'Screenshots') $app->tpl->setLoop('pkg_screenshots', $details['Screenshots']);
+        elseif($key == 'Requirements PHP settings') $app->tpl->setLoop('pkg_requirements_php_settings', $details['Requirements PHP settings']);
+    }
+}
+//print_r($details['Requirements PHP settings']);
+
+$app->tpl_defaults();
+$app->tpl->pparse();
+?>
\ No newline at end of file
diff --git a/interface/web/sites/lib/lang/en_aps.lng b/interface/web/sites/lib/lang/en_aps.lng
new file mode 100644
index 0000000..d0ecb77
--- /dev/null
+++ b/interface/web/sites/lib/lang/en_aps.lng
@@ -0,0 +1,57 @@
+<?php
+$wb['overview_txt'] = 'Overview';
+$wb['administration_txt'] = 'Administration';
+$wb['available_packages_txt'] = 'Available packages';
+$wb['installed_packages_txt'] = 'Installed packages';
+$wb['yes_txt'] = 'Yes';
+$wb['no_txt'] = 'No';
+$wb['invalid_id_txt'] = 'No valid ID has been provided.';
+$wb['details_txt'] = 'Details';
+$wb['version_txt'] = 'Version';
+$wb['category_txt'] = 'Category';
+$wb['homepage_txt'] = 'Homepage';
+$wb['supported_languages_txt'] = 'Supported languages';
+$wb['description_txt'] = 'Description';
+$wb['config_script_txt'] = 'Configuration script';
+$wb['installed_size_txt'] = 'Size after installation';
+$wb['license_txt'] = 'License';
+$wb['screenshots_txt'] = 'Screenshots';
+$wb['changelog_txt'] = 'Changelog';
+$wb['server_requirements_txt'] = 'Server requirements';
+$wb['php_extensions_txt'] = 'PHP extensions';
+$wb['php_settings_txt'] = 'PHP settings';
+$wb['supported_php_versions_txt'] = 'Supported PHP versions';
+$wb['database_txt'] = 'Database';
+$wb['settings_txt'] = 'Settings';
+$wb['install_package_txt'] = 'Install this package';
+$wb['installation_txt'] = 'Installation';
+$wb['install_location_txt'] = 'Install location';
+$wb['btn_install'] = 'Install';
+$wb['btn_cancel'] = 'Cancel';
+$wb['acceptance_txt'] = 'Acceptance';
+$wb['acceptance_text_txt'] = 'Yes, i\'ve read the license and agree.';
+$wb['install_language_txt'] = 'Interface language';
+$wb['new_database_password_txt'] = 'New database password';
+$wb['basic_settings_txt'] = 'Basic settings';
+$wb['package_settings_txt'] = 'Package settings';
+$wb['error_main_domain'] = 'The domain of the installation path is invalid.';
+$wb['error_no_main_location'] = 'You have provided no valid installation path.';
+$wb['error_inv_main_location'] = 'The given install location folder is invalid.';
+$wb['error_license_agreement'] = 'In order to continue you have to accept the license agreement.';
+$wb['error_no_database_pw'] = 'You have provided no valid database password.';
+$wb['error_short_database_pw'] = 'Please choose a longer database password.';
+$wb['error_no_value_for'] = 'The field "%s" must not be empty.';
+$wb['error_short_value_for'] = 'The field "%s" requires a longer input value.';
+$wb['error_long_value_for'] = 'The field "%s" requires a shorter input value.';
+$wb['error_inv_value_for'] = 'You have entered an invalid value for the field "%s".';
+$wb['error_inv_email_for'] = 'You have entered an invalid mail address for the field "%s".';
+$wb['error_inv_domain_for'] = 'You have entered an invalid domain for the field "%s".';
+$wb['error_inv_integer_for'] = 'You have entered an invalid number for the field "%s".';
+$wb['error_inv_float_for'] = 'You have entered an invalid floating point number for the field "%s".';
+$wb['error_used_location'] = 'The installation path already contains a package installation.';
+$wb['installation_task_txt'] = 'Install planned';
+$wb['installation_error_txt'] = 'Install error';
+$wb['installation_success_txt'] = 'Installed';
+$wb['installation_remove_txt'] = 'Removal planned';
+$wb['packagelist_update_finished_txt'] = 'APS Packagelist update finished.';
+?>
\ No newline at end of file
diff --git a/interface/web/sites/lib/lang/en_aps_instances_list.lng b/interface/web/sites/lib/lang/en_aps_instances_list.lng
new file mode 100644
index 0000000..3b9c7e9
--- /dev/null
+++ b/interface/web/sites/lib/lang/en_aps_instances_list.lng
@@ -0,0 +1,13 @@
+<?php
+$wb['list_head_txt'] = 'Installed packages';
+$wb['name_txt'] = 'Name';
+$wb['version_txt'] = 'Version';
+$wb['customer_txt'] = 'Client';
+$wb['status_txt'] = 'Status';
+$wb['install_location_txt'] = 'Install location';
+$wb['pkg_delete_confirmation'] = 'Do you really want to delete this installation?';
+$wb['pkg_reinstall_confirmation'] = 'Do you really want to reinstall this package with the same settings?';
+$wb['filter_txt'] = 'Search';
+$wb['delete_txt'] = 'Delete';
+$wb['reinstall_txt'] = 'Reinstall';
+?>
\ No newline at end of file
diff --git a/interface/web/sites/lib/lang/en_aps_packages_list.lng b/interface/web/sites/lib/lang/en_aps_packages_list.lng
new file mode 100644
index 0000000..12cc30d
--- /dev/null
+++ b/interface/web/sites/lib/lang/en_aps_packages_list.lng
@@ -0,0 +1,8 @@
+<?php
+$wb['list_head_txt'] = 'Available packages';
+$wb['name_txt'] = 'Name';
+$wb['version_txt'] = 'Version';
+$wb['category_txt'] = 'Category';
+$wb['status_txt'] = 'Unlocked';
+$wb['filter_txt'] = 'Search';
+?>
\ No newline at end of file
diff --git a/interface/web/sites/lib/module.conf.php b/interface/web/sites/lib/module.conf.php
index cbcc62b..a253dcd 100644
--- a/interface/web/sites/lib/module.conf.php
+++ b/interface/web/sites/lib/module.conf.php
@@ -153,6 +153,34 @@
 	                            'items' => $items);
 }
 
+//*** APS menu
+$items = array();
+
+$items[] = array('title'   => 'Available packages',
+                 'target'  => 'content',
+                 'link'    => 'sites/aps_availablepackages_list.php',
+                 'html_id' => 'aps_availablepackages_list');
+
+$items[] = array('title'   => 'Installed packages',
+                 'target'  => 'content',
+                 'link'    => 'sites/aps_installedpackages_list.php',
+                 'html_id' => 'aps_installedpackages_list');
+                
+
+// Second menu group, available only for admins
+if($_SESSION['s']['user']['typ'] == 'admin') 
+{
+	$items[] = array('title'   => 'Update Packagelist',
+                 'target'  => 'content',
+                 'link'    => 'sites/aps_cron_apscrawler_if.php',
+                 'html_id' => 'aps_packagedetails_show');
+}
+
+$module['nav'][] = array('title' => 'APS Installer',
+                         'open'  => 1,
+                         'items' => $items);
+
+
 //**** Statistics menu
 $items = array();
 
diff --git a/interface/web/sites/list/aps_availablepackages.list.php b/interface/web/sites/list/aps_availablepackages.list.php
new file mode 100644
index 0000000..d07b85a
--- /dev/null
+++ b/interface/web/sites/list/aps_availablepackages.list.php
@@ -0,0 +1,86 @@
+<?php
+/*
+Copyright (c) 2012, ISPConfig UG
+Contributors: web wack creations,  http://www.web-wack.at
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice,
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice,
+      this list of conditions and the following disclaimer in the documentation
+      and/or other materials provided with the distribution.
+    * Neither the name of ISPConfig nor the names of its contributors
+      may be used to endorse or promote products derived from this software without
+      specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+$liste['name'] = 'aps_packages'; // Name of the list
+$liste['table'] = 'aps_packages'; // Database table
+$liste['table_idx'] = 'id'; // Table index
+$liste["search_prefix"] = 'search_'; // Search field prefix
+$liste['records_per_page'] = 15; // Records per page
+$liste['file'] = 'aps_availablepackages_list.php'; // Script file for this list
+$liste['edit_file']    = ''; // Script file to edit
+$liste['delete_file'] = ''; // Script file to delete
+$liste['paging_tpl'] = 'templates/paging.tpl.htm'; // Paging template
+$liste['auth'] = 'no'; // Handling it myself (check for admin)
+
+// Search fields
+$liste["item"][] = array('field'    => 'name',
+                         'datatype' => 'VARCHAR',
+                         'formtype' => 'TEXT',
+                         'op'       => 'like',
+                         'prefix'   => '%',
+                         'suffix'   => '%',
+                         'width'    => '',
+                         'value'    => '');
+
+$liste["item"][] = array('field'    => 'version',
+                         'datatype' => 'VARCHAR',
+                         'formtype' => 'TEXT',
+                         'op'       => 'like',
+                         'prefix'   => '%',
+                         'suffix'   => '%',
+                         'width'    => '',
+                         'value'    => '');
+
+$liste["item"][] = array('field'    => 'category',
+                         'datatype' => 'VARCHAR',
+                         'formtype' => 'SELECT',
+                         'op'       => '=',
+                         'prefix'   => '',
+                         'suffix'   => '',
+                         'datasource' => array('type' => 'SQL',
+                                               'querystring' => 'SELECT category FROM aps_packages ORDER BY category',
+                                               'keyfield' => 'category',
+                                               'valuefield' => 'category'),
+                         'width'    => '',
+                         'value'    => '');
+
+if($_SESSION['s']['user']['typ'] == 'admin') 
+{
+$liste['item'][] = array('field'    => 'package_status',
+                         'datatype' => 'VARCHAR',
+                         'formtype' => 'SELECT',
+                         'op'       => '=',
+                         'prefix'   => '',
+                         'suffix'   => '',
+                         'width'    => '',
+                         'value'    => array(PACKAGE_ENABLED => '<div class="swap" id="ir-Yes"><span>'.$app->lng('Yes').'</span></div>',
+                                             PACKAGE_LOCKED => '<div class="swap" id="ir-No"><span>'.$app->lng('No').'</span></div>'));
+}      
+?>
\ No newline at end of file
diff --git a/interface/web/sites/list/aps_installedpackages.list.php b/interface/web/sites/list/aps_installedpackages.list.php
new file mode 100644
index 0000000..573df2a
--- /dev/null
+++ b/interface/web/sites/list/aps_installedpackages.list.php
@@ -0,0 +1,81 @@
+<?php
+/*
+Copyright (c) 2012, ISPConfig UG
+Contributors: web wack creations,  http://www.web-wack.at
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice,
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice,
+      this list of conditions and the following disclaimer in the documentation
+      and/or other materials provided with the distribution.
+    * Neither the name of ISPConfig nor the names of its contributors
+      may be used to endorse or promote products derived from this software without
+      specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+$liste['name'] = 'aps_instances'; // Name of the list
+$liste['table'] = 'aps_instances,aps_packages'; // Database table
+$liste['table_idx'] = 'id'; // Table index
+$liste["search_prefix"] = 'search_'; // Search field prefix
+$liste['records_per_page'] = 15; // Records per page
+$liste['file'] = 'aps_installedpackages_list.php'; // Script file for this list
+$liste['edit_file']    = ''; // Script file to edit
+$liste['delete_file'] = ''; // Script file to delete
+$liste['paging_tpl'] = 'templates/paging.tpl.htm'; // Paging template
+$liste['auth'] = 'no'; // Handling it myself (check for admin)
+
+// Search fields
+$liste["item"][] = array('field'    => 'name',
+                         'datatype' => 'VARCHAR',
+                         'formtype' => 'TEXT',
+                         'op'       => 'LIKE',
+                         'prefix'   => '%',
+                         'suffix'   => '%',
+                         'width'    => '',
+                         'value'    => '');
+ 
+$liste["item"][] = array('field'    => 'version',
+                         'datatype' => 'VARCHAR',
+                         'formtype' => 'TEXT',
+                         'op'       => 'like',
+                         'prefix'   => '%',
+                         'suffix'   => '%',
+                         'width'    => '',
+                         'value'    => '');
+ 
+$liste["item"][] = array('field'    => 'customer_name',
+                         'datatype' => 'VARCHAR',
+                         'formtype' => 'TEXT',
+                         'op'       => 'LIKE',
+                         'prefix'   => '%',
+                         'suffix'   => '%',
+                         'width'    => '',
+                         'value'    => '');
+                         
+$liste["item"][] = array('field'    => 'instance_status',
+                         'datatype' => 'VARCHAR',
+                         'formtype' => 'SELECT',
+                         'op'       => '=',
+                         'prefix'   => '',
+                         'suffix'   => '',
+                         'width'    => '',
+                         'value'    => array(INSTANCE_INSTALL => $app->lng('Installation_task'),
+                                             INSTANCE_ERROR => $app->lng('Installation_error'),
+                                             INSTANCE_SUCCESS => $app->lng('Installation_success'),
+                                             INSTANCE_REMOVE => $app->lng('Installation_remove'))); 
+?>
\ No newline at end of file
diff --git a/interface/web/sites/templates/aps_install_package.htm b/interface/web/sites/templates/aps_install_package.htm
new file mode 100644
index 0000000..693a460
--- /dev/null
+++ b/interface/web/sites/templates/aps_install_package.htm
@@ -0,0 +1,54 @@
+<h2>
+    {tmpl_var name='installation_txt'}: {tmpl_var name='pkg_name'} {tmpl_var name='pkg_version'}-{tmpl_var name='pkg_release'}
+    <span style="float:right">
+        <tmpl_if name='pkg_icon' op='!=' value=''>
+            <img src="{tmpl_var name='pkg_icon'}" height="32" width="32" alt="{tmpl_var name='pkg_name'}" style="vertical-align:text-bottom;" /> 
+        </tmpl_if>
+    </span>
+</h2>
+
+<tmpl_if name='error'>
+	<div id="errorMsg"><h3>ERROR</h3><ol>{tmpl_var name='error'}</ol></div>
+</tmpl_if>
+
+<div class="panel panel_install_package">
+  <div class="pnl_formsarea">
+    <fieldset class="inlineLabels">
+      <legend>{tmpl_var name='basic_settings_txt'}</legend>
+      <div class="ctrlHolder">
+      	<label for="main_domain">{tmpl_var name='install_location_txt'}</label>
+        <div class="resetButton">http(s)://&nbsp;</div>DOMAIN_LIST_SPACE<div style="float:left;">&nbsp;/&nbsp;</div>
+        <input type="text" name="main_location" id="main_location" value="{tmpl_var name='inp_main_location'}" maxlength="255" class="textInput formLengthHalf" />
+      </div>
+      <tmpl_if name='pkg_requirements_database' op='!=' value=''>
+      <div class="ctrlHolder">
+      	<label for="main_database_password">{tmpl_var name='new_database_password_txt'}</label>
+        <input type="text" class="textInput" name="main_database_password" id="main_database_password" value="{tmpl_var name='inp_main_database_password'}" size="10" maxlength="255" />
+      </div>
+      </tmpl_if>
+
+PKG_SETTINGS_SPACE
+
+      <legend>{tmpl_var name='license_txt'}</legend>
+      <div class="ctrlHolder">
+        <label for="license">{tmpl_var name='license_txt'}</label>
+        <tmpl_if name='pkg_license_content' op='==' value=''>{tmpl_var name='pkg_license_name'}<br /></tmpl_if>
+        <tmpl_if name='pkg_license_type' op='==' value='url'>
+          <a href="{tmpl_var name='pkg_license_content'}" target="_blank">{tmpl_var name='pkg_license_content'}</a>
+        <tmpl_elseif name='pkg_license_content'>
+          <textarea rows="10" cols="80" id="license_text">{tmpl_var name='pkg_license_content'}</textarea>
+        </tmpl_if>
+      </div>
+      <div class="ctrlHolder">
+        <label for="license">{tmpl_var name='acceptance_txt'}</label>
+        <input type="checkbox" name="license" id="license" <tmpl_if name='inp_license' op='==' value='true'>checked</tmpl_if> />&nbsp;&nbsp;{tmpl_var name='acceptance_text'}
+      </div>
+    </fieldset>
+
+    <input type="hidden" name="install" value="0" />
+    <div class="buttonHolder buttons">
+      <button class="positive iconstxt icoPositive" type="button" value="{tmpl_var name='btn_install'}" name="btn_install" onClick="document.pageForm.install.value=1; submitForm('pageForm','sites/aps_install_package.php?id={tmpl_var name='pkg_id'}');"><span>{tmpl_var name='btn_install'}</span></button>
+      <button class="negative iconstxt icoNegative" type="button" value="{tmpl_var name='btn_cancel'}" onClick="loadContent('aps/availablepackages_list.php');"><span>{tmpl_var name='btn_cancel'}</span></button>
+    </div>
+  </div>
+</div>
\ No newline at end of file
diff --git a/interface/web/sites/templates/aps_instances_list.htm b/interface/web/sites/templates/aps_instances_list.htm
new file mode 100644
index 0000000..6d0fb57
--- /dev/null
+++ b/interface/web/sites/templates/aps_instances_list.htm
@@ -0,0 +1,60 @@
+<h2>{tmpl_var name="list_head_txt"}</h2>
+
+<div class="panel panel_list_instances">
+  <div class="pnl_listarea">
+    <fieldset><legend>{tmpl_var name="list_head_txt"}</legend>
+      <table class="list">
+        <thead>
+          <tr>
+            <th class="tbl_col_name" scope="col">{tmpl_var name='name_txt'}</th>
+            <th class="tbl_col_version" scope="col">{tmpl_var name='version_txt'}</th>
+            <tmpl_if name='is_noclient'>
+              <th class="tbl_col_customer" scope="col">{tmpl_var name='customer_txt'}</th>
+            </tmpl_if>
+            <th class="tbl_col_installlocation" scope="col">{tmpl_var name='install_location_txt'}</th>
+            <th class="tbl_col_instancestatus" scope="col">{tmpl_var name='status_txt'}</th>
+            <th class="tbl_col_buttons" scope="col" >&nbsp;</th>
+          </tr>
+          <tr>
+            <td class="tbl_col_name"><input type="text" name="search_name" value="{tmpl_var name='search_name'}" /></td>
+            <td class="tbl_col_version"><input type="text" name="search_version" value="{tmpl_var name='search_version'}" /></td>
+            <tmpl_if name='is_noclient'>
+              <td class="tbl_col_customer"><input type="text" name="search_customer_name" value="{tmpl_var name='search_customer_name'}" /></td>
+            </tmpl_if>
+            <td class="tbl_col_installlocation">&nbsp;</td>
+            <td class="tbl_col_instancestatus"><select name="search_instance_status" onChange="submitForm('pageForm','sites/aps_installedpackages_list.php');">{tmpl_var name='search_instance_status'}</select></td>
+            <td class="tbl_col_buttons">
+              <button type="button" class="icons16 icoFilter" name="Filter" id="Filter" value="{tmpl_var name='filter_txt'}" onClick="submitForm('pageForm','sites/aps_installedpackages_list.php');">
+                <span>{tmpl_var name='filter_txt'}</span>
+              </button>
+            </td>
+          </tr>
+        </thead>
+        <tbody>
+          <tmpl_loop name='records'>
+          <tr class="tbl_row_<tmpl_if name='__EVEN__'}even<tmpl_else>uneven</tmpl_if>">
+            <td class="tbl_col_name"><a href="#" onClick="loadContent('sites/aps_packagedetails_show.php?id={tmpl_var name='package_id'}');">{tmpl_var name='package_name'}</a></td>
+            <td class="tbl_col_version">{tmpl_var name='package_version'}-{tmpl_var name='package_release'}</td>
+            <tmpl_if name='is_noclient'>
+              <td class="tbl_col_customer"><a href="#" onClick="loadContent('client/client_edit.php?id={tmpl_var name='CustomerID'}');">{tmpl_var name='customer_name'}</a></td>
+            </tmpl_if>
+            <td class="tbl_col_installlocation"><a href="http://{tmpl_var name='install_location'}" target="_blank">{tmpl_var name='install_location_short'}</a></td>
+            <td class="tbl_col_instancestatus"><span id="status_content{tmpl_var name='__ROWNUM__'}">{tmpl_var name='instance_status'}</span></td>
+            <td class="tbl_col_buttons">
+              <div class="buttons icons16" style="width:60px;">
+                <tmpl_if name='delete_possible'><a class="icons16 icoDelete" href="javascript:if(window.confirm('{tmpl_var name='pkg_delete_confirmation'}')){ loadContentInto('status_content{tmpl_var name='__ROWNUM__'}', 'sites/aps_do_operation.php?action=delete_instance&id={tmpl_var name='id'}&phpsessid={tmpl_var name='phpsessid'}'); }"><span>{tmpl_var name='delete_txt'}</span></a></tmpl_if>
+                <tmpl_if name='reinstall_possible'><a class="icons16 icoEdit" href="javascript:if(window.confirm('{tmpl_var name='pkg_reinstall_confirmation'}')){ loadContentInto('status_content{tmpl_var name='__ROWNUM__'}', 'sites/aps_do_operation.php?action=reinstall_instance&id={tmpl_var name='id'}&phpsessid={tmpl_var name='phpsessid'}'); }"><span>{tmpl_var name='reinstall_txt'}</span></a></tmpl_if>
+              </div>
+            </td>        
+          </tr>
+          </tmpl_loop>
+        </tbody>
+        <tfoot>
+          <tr>
+            <td class="tbl_footer tbl_paging" colspan="6">{tmpl_var name='paging'}</td>
+          </tr>
+        </tfoot>
+      </table>
+    </fieldset>
+  </div>
+</div>
\ No newline at end of file
diff --git a/interface/web/sites/templates/aps_packagedetails_show.htm b/interface/web/sites/templates/aps_packagedetails_show.htm
new file mode 100644
index 0000000..267a1ea
--- /dev/null
+++ b/interface/web/sites/templates/aps_packagedetails_show.htm
@@ -0,0 +1,122 @@
+<h2>
+    <tmpl_if name='pkg_icon' op='!=' value=''>
+        <img src="{tmpl_var name='pkg_icon'}" height="32" width="32" alt="{tmpl_var name='pkg_name'}" style="vertical-align:text-bottom;" /> 
+    </tmpl_if>
+    {tmpl_var name='pkg_name'}
+</h2>
+<b>{tmpl_var name='pkg_summary'}</b>
+<p>&nbsp;</p>
+
+<div class="pnl_toolsarea">
+  <div class="buttons">
+    <button class="iconstxt icoAdd" type="button" onClick="loadContent('sites/aps_install_package.php?id={tmpl_var name='pkg_id'}');">
+      <span>{tmpl_var name='install_package_txt'}</span>
+    </button>
+  </div>
+  <p>&nbsp;</p><p>&nbsp;</p>
+</ddiv>
+
+
+<div class="tabbox_tabs">
+    <input type="hidden" name="next_tab" value="" />
+    <ul>
+        <li<tmpl_if name='next_tab' op='==' value='details'> class="active"</tmpl_if>>
+            <a href="javascript:changeTab('details', 'sites/aps_packagedetails_show.php?id={tmpl_var name='pkg_id'}');">{tmpl_var name='details_txt'}</a>
+        </li>
+        <tmpl_if name='pkg_screenshots'>
+        <li<tmpl_if name='next_tab' op='==' value='screenshots'> class="active"</tmpl_if>>
+            <a href="javascript:changeTab('screenshots', 'sites/aps_packagedetails_show.php?id={tmpl_var name='pkg_id'}');">{tmpl_var name='screenshots_txt'}</a>
+        </li></tmpl_if>
+        <tmpl_if name='pkg_changelog'>
+        <li<tmpl_if name='next_tab' op='==' value='changelog'> class="active"</tmpl_if>>
+            <a href="javascript:changeTab('changelog', 'sites/aps_packagedetails_show.php?id={tmpl_var name='pkg_id'}');">{tmpl_var name='changelog_txt'}</a>
+        </li></tmpl_if>
+        <li<tmpl_if name='next_tab' op='==' value='settings'> class="active"</tmpl_if>>
+            <a href="javascript:changeTab('settings', 'sites/aps_packagedetails_show.php?id={tmpl_var name='pkg_id'}');">{tmpl_var name='settings_txt'}</a>
+        </li>
+    </ul>
+</div>
+<p>&nbsp;</p>
+
+<div class="panel panel_list_packages">
+  <div class="pnl_listarea">
+    <table class="list">
+      <tbody>
+      <tmpl_if name='next_tab' op='==' value='details'>
+        <tr class="tbl_row_uneven">
+          <td width="25%">{tmpl_var name='version_txt'}</td>
+          <td>{tmpl_var name='pkg_version'} (Release {tmpl_var name='pkg_release'})</td>
+        </tr>
+        <tr class="tbl_row_even">
+          <td>{tmpl_var name='category_txt'}</td>
+          <td>{tmpl_var name='pkg_category'}</td>
+        </tr>
+        <tr class="tbl_row_uneven">
+          <td>{tmpl_var name='description_txt'}</td>
+          <td>{tmpl_var name='pkg_description'}</td>
+        </tr>
+        <tr class="tbl_row_even">
+          <td>{tmpl_var name='homepage_txt'}</td>
+          <td><a href="{tmpl_var name='pkg_homepage'}" target="_blank">{tmpl_var name='pkg_homepage'}</a></td>
+        </tr>
+        <tr class="tbl_row_uneven">
+          <td>{tmpl_var name='installed_size_txt'}</td>
+          <td>{tmpl_var name='pkg_installed_size'}</td>
+        </tr>
+        <tr class="tbl_row_even">
+            <td>{tmpl_var name='supported_languages_txt'}</td>
+            <td>{tmpl_var name='pkg_languages'}</td>
+          </tr>
+        <tr class="tbl_row_uneven">
+          <td>{tmpl_var name='config_script_txt'}</td>
+          <td>{tmpl_var name='pkg_config_script'}</td>
+        </tr>
+        <tr class="tbl_row_even">
+          <td>{tmpl_var name='license_txt'}</td>
+          <td>
+              <tmpl_if name='pkg_license_name'>{tmpl_var name='pkg_license_name'}<br /></tmpl_if>
+              <tmpl_if name='pkg_license_type' op='==' value='url'>
+                  <a href="{tmpl_var name='pkg_license_content'}" target="_blank">{tmpl_var name='pkg_license_content'}</a>
+              <tmpl_elseif name='pkg_license_content'>
+                  <textarea rows="10" cols="80">{tmpl_var name='pkg_license_content'}</textarea>
+              </tmpl_if>
+          </td>
+        </tr>
+      <tmpl_elseif name='next_tab' op='==' value='screenshots'>
+        <tmpl_if name='pkg_screenshots'><tr class="tbl_row_even">
+          <td style="text-align:center;"><tmpl_loop name='pkg_screenshots'>
+              <img src="{tmpl_var name='ScreenPath'}" alt="{tmpl_var name='ScreenDescription'}" /><br />
+              <em>{tmpl_var name='ScreenDescription'}</em><br /><br />
+          </tmpl_loop></td>
+        </tr></tmpl_if>
+      <tmpl_elseif name='next_tab' op='==' value='changelog'>
+        <tmpl_if name='pkg_changelog'><tr class="tbl_row_even">
+          <td><ul><tmpl_loop name='pkg_changelog'>
+                <li>{tmpl_var name='ChangelogVersion'}</li>
+              <ul><tmpl_if name='ChangelogDescription'><li>{tmpl_var name='ChangelogDescription'}</li></tmpl_if></ul>
+          </tmpl_loop></ul></td>
+        </tr></tmpl_if>
+      <tmpl_elseif name='next_tab' op='==' value='settings'>
+        <tr class="tbl_row_uneven">
+          <td width="25%">{tmpl_var name='php_extensions_txt'}</td>
+          <td>{tmpl_var name='pkg_requirements_php_extensions'}</td>
+        </tr>
+        <tr class="tbl_row_even">
+          <td>{tmpl_var name='php_settings_txt'}</td>
+          <td><tmpl_loop name='pkg_requirements_php_settings'>
+                {tmpl_var name='PHPSettingName'} = {tmpl_var name='PHPSettingValue'}<br />
+              </tmpl_loop></td>
+        </tr>
+        <tr class="tbl_row_uneven">
+          <td>{tmpl_var name='supported_php_versions_txt'}</td>
+          <td>{tmpl_var name='pkg_requirements_supported_php_versions'}</td>
+        </tr>
+        <tr class="tbl_row_even">
+          <td>{tmpl_var name='database_txt'}</td>
+          <td>{tmpl_var name='pkg_requirements_database'}</a></td>
+        </tr>
+      </tmpl_if>
+      </tbody>
+    </table>
+  </div>
+</div>
\ No newline at end of file
diff --git a/interface/web/sites/templates/aps_packages_list.htm b/interface/web/sites/templates/aps_packages_list.htm
new file mode 100644
index 0000000..941a8b1
--- /dev/null
+++ b/interface/web/sites/templates/aps_packages_list.htm
@@ -0,0 +1,48 @@
+<h2>{tmpl_var name="list_head_txt"}</h2>
+
+<div class="panel panel_list_packages">
+  <div class="pnl_listarea">
+    <fieldset><legend>{tmpl_var name="list_head_txt"} ({tmpl_var name='package_count'})</legend>
+      <table class="list">
+        <thead>
+          <tr>
+            <th class="tbl_col_name" scope="col">{tmpl_var name='name_txt'}</th>
+            <th class="tbl_col_version" scope="col">{tmpl_var name='version_txt'}</th>
+            <th class="tbl_col_category" scope="col">{tmpl_var name='category_txt'}</th>
+            <tmpl_if name='is_admin'>
+              <th class="tbl_col_status" scope="col">{tmpl_var name='status_txt'}</th>
+            </tmpl_if>
+            <th class="tbl_col_buttons" scope="col" width="60px;">&nbsp;</th>
+          </tr>
+          <tr>
+            <td class="tbl_col_name"><input type="text" name="search_name" value="{tmpl_var name='search_name'}" /></td>
+            <td class="tbl_col_version"><input type="text" name="search_version" value="{tmpl_var name='search_version'}" /></td>
+            <td class="tbl_col_customerid"><select name="search_category" onChange="submitForm('pageForm','sites/aps_availablepackages_list.php');">{tmpl_var name='search_category'}</select></td>
+            <tmpl_if name='is_admin'>
+              <td class="tbl_col_status"><select name="search_package_status" onChange="submitForm('pageForm','sites/aps_availablepackages_list.php');">{tmpl_var name='search_package_status'}</select></td>
+            </tmpl_if>
+            <td class="tbl_col_buttons"><button type="button" class="icons16 icoFilter" name="Filter" id="Filter" value="{tmpl_var name='filter_txt'}" onClick="submitForm('pageForm','sites/aps_availablepackages_list.php');"><span>{tmpl_var name='filter_txt'}</span></button></td>
+          </tr>
+        </thead>
+        <tbody>
+          <tmpl_loop name='records'>
+          <tr class="tbl_row_<tmpl_if name='__EVEN__'}even<tmpl_else>uneven</tmpl_if>">
+            <td class="tbl_col_name"><a href="#" onClick="loadContent('sites/aps_packagedetails_show.php?id={tmpl_var name='id'}');">{tmpl_var name='name'}</a></td>
+            <td class="tbl_col_version">{tmpl_var name='version'}-{tmpl_var name='release'}</td>
+            <td class="tbl_col_category">{tmpl_var name='category'}</td>
+            <tmpl_if name='is_admin'>
+              <td class="tbl_col_status"><a href="javascript:loadContentInto('status_content{tmpl_var name='__ROWNUM__'}', 'sites/aps_do_operation.php?action=change_status&id={tmpl_var name='id'}&phpsessid={tmpl_var name='phpsessid'}');"><span id="status_content{tmpl_var name='__ROWNUM__'}">{tmpl_var name='package_status'}</span></a></td>
+            </tmpl_if>
+            <td>&nbsp;</td>
+          </tr>
+          </tmpl_loop>
+        </tbody>
+        <tfoot>
+          <tr>
+            <td class="tbl_footer tbl_paging" colspan="5">{tmpl_var name='paging'}</td>
+          </tr>
+        </tfoot>
+      </table>
+    </fieldset>
+  </div>
+</div>
diff --git a/interface/web/themes/default/css/screen/content_ispc.css b/interface/web/themes/default/css/screen/content_ispc.css
index b3d9354..2678187 100644
--- a/interface/web/themes/default/css/screen/content_ispc.css
+++ b/interface/web/themes/default/css/screen/content_ispc.css
@@ -381,7 +381,10 @@
 	.icons16.icoLoginAs { background-image: url("../../icons/x16/user_go.png"); }
 	.icons16.icoWebmailer { background-image: url("../../icons/x16/mails_arrow.png"); }
 
-
+	#ajaxloader {
+		text-align:center;
+		margin-top: 180px;
+	}
 
 	.blockLabel.email_at {
 		width: 20px !important;
diff --git a/interface/web/themes/default/images/ajax-loader.gif b/interface/web/themes/default/images/ajax-loader.gif
new file mode 100644
index 0000000..7c4804e
--- /dev/null
+++ b/interface/web/themes/default/images/ajax-loader.gif
Binary files differ

--
Gitblit v1.9.1