florian030
2014-02-25 cce9d63279d9078dd0292dd015be497600ac5f32
server/lib/classes/aps_installer.inc.php
@@ -27,303 +27,318 @@
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');
require_once 'aps_base.inc.php';
@set_time_limit(0);
@ignore_user_abort(1);
class ApsInstaller extends ApsBase
{
    private $handle_type = '';
    private $domain = '';
    private $document_root = '';
    private $sublocation = '';
    private $local_installpath = '';
    private $dbhost = '';
    private $newdb_name = '';
    private $newdb_user = '';
    private $file_owner_user = '';
    private $file_owner_group = '';
   private $handle_type = '';
   private $domain = '';
   private $document_root = '';
   private $sublocation = '';
   private $local_installpath = '';
   private $dbhost = '';
   private $newdb_name = '';
   private $newdb_user = '';
   private $file_owner_user = '';
   private $file_owner_group = '';
   private $putenv = array();
   /**
    * 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 installer: ', $interface_mode);
    }
    /**
     * Before the cron is executed, make sure all necessary options are set
     * and all functions are available
     */
    private function checkRequirements()
    {
        global $app;
        try
        {
            // Check if exec() is not disabled
            $disabled_func = explode(',', @ini_get('disable_functions'));
            if(in_array('exec', $disabled_func)) throw new Exception('the call of exec() is disabled');
            // Check if safe_mode is disabled (needed for correct putenv, chmod, chown handling)
            if(@ini_get('safe_mode')) throw new Exception('the safe_mode restriction is on');
            return true;
        }
        catch(Exception $e)
        {
            $app->log('Aborting execution because '.$e->getMessage(), 1);
            return false;
        }
    }
    /**
     * Get a file from a ZIP archive and either return it's content or
     * extract it to a given destination
     *
     * @param $zipfile the ZIP file to work with
     * @param $subfile the file from which to get the content
     * @param $destfolder the optional extraction destination
     * @param $destname the optional target file name when extracting
     * @return string or boolean
     */
    private function getContentFromZIP($zipfile, $subfile, $destfolder = '', $destname = '')
    {
        try
        {
            $zip = new ZipArchive;
            $res = $zip->open(realpath($zipfile));
            if(!$res) throw new Exception('Cannot open ZIP file '.$zipfile);
            // If no destination is given, the content is returned, otherwise
            // the $subfile is extracted to $destination
            if($destfolder == '')
            {
                $fh = $zip->getStream($subfile);
                if(!$fh) throw new Exception('Cannot read '.$subfile.' from '.$zipfile);
                $subfile_content = '';
                while(!feof($fh)) $subfile_content .= fread($fh, 8192);
                fclose($fh);
                return $subfile_content;
            }
            else
            {
                // extractTo would be suitable but has no target name parameter
                //$ind = $zip->locateName($subfile);
                //$ex = $zip->extractTo($destination, array($zip->getNameIndex($ind)));
                if($destname == '') $destname = basename($subfile);
                $ex = @copy('zip://'.$zipfile.'#'.$subfile, $destfolder.$destname);
                if(!$ex) throw new Exception('Cannot extract '.$subfile.' to '.$destfolder);
            }
            $zip->close();
        }
        catch(Exception $e)
        {
            // The exception message is only interesting for debugging reasons
            // echo $e->getMessage();
            return false;
        }
    }
    /**
     * Extract the complete directory of a ZIP file
     *
     * @param $filename the file to unzip
     * @param $directory the ZIP inside directory to unzip
     * @param $destination the place where to extract the data
     * @return boolean
     */
    private function extractZip($filename, $directory, $destination)
    {
        if(!file_exists($filename)) return false;
        // Fix the paths
        if(substr($directory, -1) == '/') $directory = substr($directory, 0, strlen($directory) - 1);
        if(substr($destination, -1) != '/') $destination .= '/';
        // Read and extract the ZIP file
        $ziphandle = zip_open(realpath($filename));
        if(is_resource($ziphandle))
        {
            while($entry = zip_read($ziphandle))
            {
                if(substr(zip_entry_name($entry), 0, strlen($directory)) == $directory)
                {
                    // Modify the relative ZIP file path
                    $new_path = substr(zip_entry_name($entry), strlen($directory));
                    if(substr($new_path, -1) == '/') // Identifier for directories
                    {
                        if(!file_exists($destination.$new_path)) mkdir($destination.$new_path, 0777, true);
                    }
                    else // Handle files
                    {
                        if(zip_entry_open($ziphandle, $entry))
                        {
                            $new_dir = dirname($destination.$new_path);
                            if(!file_exists($new_dir)) mkdir($new_dir, 0777, true);
                            $file = fopen($destination.$new_path, 'wb');
                            if($file)
                            {
                                while($line = zip_entry_read($entry)) fwrite($file, $line);
                                fclose($file);
                            }
                            else return false;
                        }
                    }
                }
            }
            zip_close($ziphandle);
            return true;
        }
        return false;
    }
    /**
     * Setup the path environment variables for the install script
     *
     * @param $parent_mapping the SimpleXML instance with the current mapping position
     * @param $url the relative path within the mapping tree
     * @param $path the absolute path within the mapping tree
     */
    private function processMappings($parent_mapping, $url, $path)
    {
        if($parent_mapping && $parent_mapping != null)
        {
            $writable = parent::getXPathValue($parent_mapping, 'php:permissions/@writable');
            $readable = parent::getXPathValue($parent_mapping, 'php:permissions/@readable');
   /**
    * Constructor
    *
    * @param $app the application instance (db handle + log method)
    * @param $interface_mode act in interface (true) or server mode (false)
    */
            // set the write permission
            if($writable == 'true')
            {
                if(is_dir($path)) chmod($path, 0775);
                else chmod($path, 0664);
            }
            // set non-readable permission
            if($readable == 'false')
            {
                if(is_dir($path)) chmod($path, 0333);
                else chmod($path, 0222);
            }
        }
        // Set the environment variables
        $env = str_replace('/', '_', $url);
        $this->putenv[] = 'WEB_'.$env.'_DIR='.$path;
        // Step recursively into further mappings
        if($parent_mapping && $parent_mapping != null)
        {
            foreach($parent_mapping->mapping as $mapping)
            {
                if($url == '/') $this->processMappings($mapping, $url.$mapping['url'], $path.$mapping['url']);
                else $this->processMappings($mapping, $url.'/'.$mapping['url'], $path.'/'.$mapping['url']);
            }
        }
    }
    /**
     * Setup the environment with data for the install location
     *
     * @param $task an array containing all install related data
     */
    private function prepareLocation($task)
    {
        global $app;
        // Get the domain name to use for the installation
        // Would be possible in one query too, but we use 2 for easier debugging
        $main_domain = $app->db->queryOneRecord("SELECT value FROM aps_instances_settings
   public function __construct($app, $interface_mode = false)
   {
      parent::__construct($app, 'APS installer: ', $interface_mode);
   }
   /**
    * Before the cron is executed, make sure all necessary options are set
    * and all functions are available
    */
   private function checkRequirements()
   {
      global $app;
      try
      {
         // Check if exec() is not disabled
         $disabled_func = explode(',', @ini_get('disable_functions'));
         if(in_array('exec', $disabled_func)) throw new Exception('the call of exec() is disabled');
         // Check if safe_mode is disabled (needed for correct putenv, chmod, chown handling)
         if(@ini_get('safe_mode')) throw new Exception('the safe_mode restriction is on');
         return true;
      }
      catch(Exception $e)
      {
         $app->log('Aborting execution because '.$e->getMessage(), 1);
         return false;
      }
   }
   /**
    * Get a file from a ZIP archive and either return it's content or
    * extract it to a given destination
    *
    * @param $zipfile the ZIP file to work with
    * @param $subfile the file from which to get the content
    * @param $destfolder the optional extraction destination
    * @param $destname the optional target file name when extracting
    * @return string or boolean
    */
   private function getContentFromZIP($zipfile, $subfile, $destfolder = '', $destname = '')
   {
      try
      {
         $zip = new ZipArchive;
         $res = $zip->open(realpath($zipfile));
         if(!$res) throw new Exception('Cannot open ZIP file '.$zipfile);
         // If no destination is given, the content is returned, otherwise
         // the $subfile is extracted to $destination
         if($destfolder == '')
         {
            $fh = $zip->getStream($subfile);
            if(!$fh) throw new Exception('Cannot read '.$subfile.' from '.$zipfile);
            $subfile_content = '';
            while(!feof($fh)) $subfile_content .= fread($fh, 8192);
            fclose($fh);
            return $subfile_content;
         }
         else
         {
            // extractTo would be suitable but has no target name parameter
            //$ind = $zip->locateName($subfile);
            //$ex = $zip->extractTo($destination, array($zip->getNameIndex($ind)));
            if($destname == '') $destname = basename($subfile);
            $ex = @copy('zip://'.$zipfile.'#'.$subfile, $destfolder.$destname);
            if(!$ex) throw new Exception('Cannot extract '.$subfile.' to '.$destfolder);
         }
         $zip->close();
      }
      catch(Exception $e)
      {
         // The exception message is only interesting for debugging reasons
         // echo $e->getMessage();
         return false;
      }
   }
   /**
    * Extract the complete directory of a ZIP file
    *
    * @param $filename the file to unzip
    * @param $directory the ZIP inside directory to unzip
    * @param $destination the place where to extract the data
    * @return boolean
    */
   private function extractZip($filename, $directory, $destination)
   {
      if(!file_exists($filename)) return false;
      // Fix the paths
      if(substr($directory, -1) == '/') $directory = substr($directory, 0, strlen($directory) - 1);
      if(substr($destination, -1) != '/') $destination .= '/';
      // Read and extract the ZIP file
      $ziphandle = zip_open(realpath($filename));
      if(is_resource($ziphandle))
      {
         while($entry = zip_read($ziphandle))
         {
            if(substr(zip_entry_name($entry), 0, strlen($directory)) == $directory)
            {
               // Modify the relative ZIP file path
               $new_path = substr(zip_entry_name($entry), strlen($directory));
               if(substr($new_path, -1) == '/') // Identifier for directories
                  {
                  if(!file_exists($destination.$new_path)) mkdir($destination.$new_path, 0777, true);
               }
               else // Handle files
                  {
                  if(zip_entry_open($ziphandle, $entry))
                  {
                     $new_dir = dirname($destination.$new_path);
                     if(!file_exists($new_dir)) mkdir($new_dir, 0777, true);
                     $file = fopen($destination.$new_path, 'wb');
                     if($file)
                     {
                        while($line = zip_entry_read($entry)) fwrite($file, $line);
                        fclose($file);
                     }
                     else return false;
                  }
               }
            }
         }
         zip_close($ziphandle);
         return true;
      }
      return false;
   }
   /**
    * Setup the path environment variables for the install script
    *
    * @param $parent_mapping the SimpleXML instance with the current mapping position
    * @param $url the relative path within the mapping tree
    * @param $path the absolute path within the mapping tree
    */
   private function processMappings($parent_mapping, $url, $path)
   {
      if($parent_mapping && $parent_mapping != null)
      {
         $writable = parent::getXPathValue($parent_mapping, 'php:permissions/@writable');
         $readable = parent::getXPathValue($parent_mapping, 'php:permissions/@readable');
         // set the write permission
         if($writable == 'true')
         {
            if(is_dir($path)) chmod($path, 0775);
            else chmod($path, 0664);
         }
         // set non-readable permission
         if($readable == 'false')
         {
            if(is_dir($path)) chmod($path, 0333);
            else chmod($path, 0222);
         }
      }
      // Set the environment variables
      $env = str_replace('/', '_', $url);
      $this->putenv[] = 'WEB_'.$env.'_DIR='.$path;
      // Step recursively into further mappings
      if($parent_mapping && $parent_mapping != null)
      {
         foreach($parent_mapping->mapping as $mapping)
         {
            if($url == '/') $this->processMappings($mapping, $url.$mapping['url'], $path.$mapping['url']);
            else $this->processMappings($mapping, $url.'/'.$mapping['url'], $path.'/'.$mapping['url']);
         }
      }
   }
   /**
    * Setup the environment with data for the install location
    *
    * @param $task an array containing all install related data
    */
   private function prepareLocation($task)
   {
      global $app;
      // Get the domain name to use for the installation
      // Would be possible in one query too, but we use 2 for easier debugging
      $main_domain = $app->db->queryOneRecord("SELECT value FROM aps_instances_settings
            WHERE name = 'main_domain' AND instance_id = '".$app->db->quote($task['instance_id'])."';");
        $this->domain = $main_domain['value'];
        // Get the document root
        $domain_res = $app->db->queryOneRecord("SELECT document_root, web_folder, type FROM web_domain
            WHERE domain = '".$app->db->quote($this->domain)."';");
        $this->document_root = $domain_res['document_root'];
        // Get the sub location
        $location_res = $app->db->queryOneRecord("SELECT value FROM aps_instances_settings
            WHERE name = 'main_location' AND instance_id = '".$app->db->quote($task['instance_id'])."';");
        $this->sublocation = $location_res['value'];
        // Make sure the document_root ends with /
        if(substr($this->document_root, -1) != '/') $this->document_root .= '/';
        // Attention: ISPConfig Special: web files are in subfolder 'web' -> append it:
        if($domain_res['type'] == 'vhostsubdomain') $this->document_root .= $domain_res['web_folder'] . '/';
        else $this->document_root .= 'web/';
      $this->domain = $main_domain['value'];
        // If a subfolder is given, make sure it's path doesn't begin with / i.e. /phpbb
        if(substr($this->sublocation, 0, 1) == '/') $this->sublocation = substr($this->sublocation, 1);
        // If the package isn't installed to a subfolder, remove the / at the end of the document root
        if(empty($this->sublocation)) $this->document_root = substr($this->document_root, 0, strlen($this->document_root) - 1);
        // Set environment variables, later processed by the package install script
        $this->putenv[] = 'BASE_URL_SCHEME=http';
        // putenv('BASE_URL_PORT') -> omitted as it's 80 by default
        $this->putenv[] = 'BASE_URL_HOST='.$this->domain;
        $this->putenv[] = 'BASE_URL_PATH='.$this->sublocation.'/';
    }
    /**
     * Setup a database (if needed) and the appropriate environment variables
     *
     * @param $task an array containing all install related data
     * @param $sxe a SimpleXMLElement handle, holding APP-META.xml
     */
    private function prepareDatabase($task, $sxe)
    {
        global $app;
        $db_id = parent::getXPathValue($sxe, '//db:id');
        if(empty($db_id)) return; // No database needed
      // Get the document root
      $domain_res = $app->db->queryOneRecord("SELECT document_root, web_folder, type FROM web_domain
            WHERE domain = '".$app->db->quote($this->domain)."';");
      $this->document_root = $domain_res['document_root'];
      // Get the sub location
      $location_res = $app->db->queryOneRecord("SELECT value FROM aps_instances_settings
            WHERE name = 'main_location' AND instance_id = '".$app->db->quote($task['instance_id'])."';");
      $this->sublocation = $location_res['value'];
      // Make sure the document_root ends with /
      if(substr($this->document_root, -1) != '/') $this->document_root .= '/';
      // Attention: ISPConfig Special: web files are in subfolder 'web' -> append it:
        if(($domain_res['type'] == 'vhostsubdomain') || ($domain_res['type'] == 'vhostalias'))
           $this->document_root .= $domain_res['web_folder'] . '/';
      else $this->document_root .= 'web/';
      // If a subfolder is given, make sure it's path doesn't begin with / i.e. /phpbb
      if(substr($this->sublocation, 0, 1) == '/') $this->sublocation = substr($this->sublocation, 1);
      // If the package isn't installed to a subfolder, remove the / at the end of the document root
      if(empty($this->sublocation)) $this->document_root = substr($this->document_root, 0, strlen($this->document_root) - 1);
      // Set environment variables, later processed by the package install script
      $this->putenv[] = 'BASE_URL_SCHEME=http';
      // putenv('BASE_URL_PORT') -> omitted as it's 80 by default
      $this->putenv[] = 'BASE_URL_HOST='.$this->domain;
      $this->putenv[] = 'BASE_URL_PATH='.$this->sublocation.'/';
   }
   /**
    * Setup a database (if needed) and the appropriate environment variables
    *
    * @param $task an array containing all install related data
    * @param $sxe a SimpleXMLElement handle, holding APP-META.xml
    */
   private function prepareDatabase($task, $sxe)
   {
      global $app;
      $db_id = parent::getXPathValue($sxe, '//db:id');
      if(empty($db_id)) return; // No database needed
      /* WARNING: if this will ever be uncommented please check the updated prefix handling for user and db names!!!
         *
         *
        // Set the database owner to the domain owner
        // ISPConfig identifies the owner by the sys_groupid (not sys_userid!)
        // so sys_userid can be set to any value
        $perm = $app->db->queryOneRecord("SELECT sys_groupid, server_id FROM web_domain
        $perm = $app->db->queryOneRecord("SELECT sys_groupid, server_id FROM web_domain
            WHERE domain = '".$this->domain."';");
        $task['sys_groupid'] = $perm['sys_groupid'];
        $serverid = $perm['server_id'];
        // Get the database prefix and db user prefix
        // Get the database prefix and db user prefix
        $app->uses('getconf');
        $global_config = $app->getconf->get_global_config('sites');
        $dbname_prefix = str_replace('[CLIENTID]', '', $global_config['dbname_prefix']);
        $dbuser_prefix = str_replace('[CLIENTID]', '', $global_config['dbuser_prefix']);
        $this->dbhost = DB_HOST; // Taken from config.inc.php
        if(empty($this->dbhost)) $this->dbhost = 'localhost'; // Just to ensure any hostname... ;)
        $this->newdb_name = $dbname_prefix.$task['CustomerID'].'aps'.$task['InstanceID'];
        $this->newdb_user = $dbuser_prefix.$task['CustomerID'].'aps'.$task['InstanceID'];
        $dbpw_res = $app->db->queryOneRecord("SELECT Value FROM aps_instances_settings
        $dbpw_res = $app->db->queryOneRecord("SELECT Value FROM aps_instances_settings
            WHERE Name = 'main_database_password' AND InstanceID = '".$app->db->quote($task['InstanceID'])."';");
        $newdb_pw = $dbpw_res['Value'];
        // In any case delete an existing database (install and removal procedure)
        $app->db->query('DROP DATABASE IF EXISTS `'.$app->db->quote($this->newdb_name).'`;');
        // Delete an already existing database with this name
        $app->db->query("DELETE FROM web_database WHERE database_name = '".$app->db->quote($this->newdb_name)."';");
        // Create the new database and assign it to a user
        if($this->handle_type == 'install')
        {
@@ -331,77 +346,94 @@
            $app->db->query('GRANT ALL PRIVILEGES ON '.$app->db->quote($this->newdb_name).'.* TO '.$app->db->quote($this->newdb_user).'@'.$app->db->quote($this->dbhost).' IDENTIFIED BY \'password\';');
            $app->db->query('SET PASSWORD FOR '.$app->db->quote($this->newdb_user).'@'.$app->db->quote($this->dbhost).' = PASSWORD(\''.$newdb_pw.'\');');
            $app->db->query('FLUSH PRIVILEGES;');
            // Add the new database to the customer databases
            // Assumes: charset = utf8
            $app->db->query('INSERT INTO web_database (sys_userid, sys_groupid, sys_perm_user, sys_perm_group, sys_perm_other, server_id,
                type, database_name, database_user, database_password, database_charset, remote_access, remote_ips, active)
                VALUES ('.$task['sys_userid'].', '.$task['sys_groupid'].', "'.$task['sys_perm_user'].'", "'.$task['sys_perm_group'].'",
                "'.$task['sys_perm_other'].'", '.$app->db->quote($serverid).', "mysql", "'.$app->db->quote($this->newdb_name).'",
            $app->db->query('INSERT INTO web_database (sys_userid, sys_groupid, sys_perm_user, sys_perm_group, sys_perm_other, server_id,
                type, database_name, database_user, database_password, database_charset, remote_access, remote_ips, active)
                VALUES ('.$task['sys_userid'].', '.$task['sys_groupid'].', "'.$task['sys_perm_user'].'", "'.$task['sys_perm_group'].'",
                "'.$task['sys_perm_other'].'", '.$app->db->quote($serverid).', "mysql", "'.$app->db->quote($this->newdb_name).'",
                "'.$app->db->quote($this->newdb_user).'", "'.$app->db->quote($newdb_pw).'", "utf8", "n", "", "y");');
        }
      */
        $mysqlver_res = $app->db->queryOneRecord('SELECT VERSION() as ver;');
        $mysqlver = $mysqlver_res['ver'];
      $mysqlver_res = $app->db->queryOneRecord('SELECT VERSION() as ver;');
      $mysqlver = $mysqlver_res['ver'];
      $tmp = $app->db->queryOneRecord("SELECT value FROM aps_instances_settings WHERE name = 'main_database_password' AND instance_id = '".$app->db->quote($task['instance_id'])."';");
        $newdb_pw = $tmp['value'];
      $newdb_pw = $tmp['value'];
      $tmp = $app->db->queryOneRecord("SELECT value FROM aps_instances_settings WHERE name = 'main_database_host' AND instance_id = '".$app->db->quote($task['instance_id'])."';");
        $newdb_host = $tmp['value'];
      $newdb_host = $tmp['value'];
      $tmp = $app->db->queryOneRecord("SELECT value FROM aps_instances_settings WHERE name = 'main_database_name' AND instance_id = '".$app->db->quote($task['instance_id'])."';");
        $newdb_name = $tmp['value'];
      $newdb_name = $tmp['value'];
      $tmp = $app->db->queryOneRecord("SELECT value FROM aps_instances_settings WHERE name = 'main_database_login' AND instance_id = '".$app->db->quote($task['instance_id'])."';");
        $newdb_login = $tmp['value'];
        $this->putenv[] = 'DB_'.$db_id.'_TYPE=mysql';
        $this->putenv[] = 'DB_'.$db_id.'_NAME='.$newdb_name;
        $this->putenv[] = 'DB_'.$db_id.'_LOGIN='.$newdb_login;
        $this->putenv[] = 'DB_'.$db_id.'_PASSWORD='.$newdb_pw;
        $this->putenv[] = 'DB_'.$db_id.'_HOST='.$newdb_host;
        $this->putenv[] = 'DB_'.$db_id.'_PORT=3306';
        $this->putenv[] = 'DB_'.$db_id.'_VERSION='.$mysqlver;
    }
    /**
     * Extract all needed files from the package
     *
     * @param $task an array containing all install related data
     * @param $sxe a SimpleXMLElement handle, holding APP-META.xml
     * @return boolean
     */
    private function prepareFiles($task, $sxe)
    {
        global $app;
        // Basically set the mapping for APS version 1.0, if not available -> newer way
        $mapping = $sxe->mapping;
        $mapping_path = $sxe->mapping['path'];
        $mapping_url = $sxe->mapping['url'];
        if(empty($mapping))
        {
            $mapping = $sxe->service->provision->{'url-mapping'}->mapping;
            $mapping_path = $sxe->service->provision->{'url-mapping'}->mapping['path'];
            $mapping_url = $sxe->service->provision->{'url-mapping'}->mapping['url'];
        }
      $newdb_login = $tmp['value'];
      /* Test if the new mysql connection is laready working to ensure that db servers in multiserver
         setups get enough time to create the database */
      if($this->handle_type == 'install') {
         for($n = 1; $n < 15; $n++) {
            $link = mysql_connect($newdb_host, $newdb_login, $newdb_pw);
            if (!$link) {
               unset($link);
               sleep(5);
            } else {
               unset($link);
               break;
            }
         }
      }
        try
        {
            // Make sure we have a valid mapping path (at least /)
            if(empty($mapping_path)) throw new Exception('Unable to determine a mapping path');
            $this->local_installpath = $this->document_root.$this->sublocation.'/';
      $this->putenv[] = 'DB_'.$db_id.'_TYPE=mysql';
      $this->putenv[] = 'DB_'.$db_id.'_NAME='.$newdb_name;
      $this->putenv[] = 'DB_'.$db_id.'_LOGIN='.$newdb_login;
      $this->putenv[] = 'DB_'.$db_id.'_PASSWORD='.$newdb_pw;
      $this->putenv[] = 'DB_'.$db_id.'_HOST='.$newdb_host;
      $this->putenv[] = 'DB_'.$db_id.'_PORT=3306';
      $this->putenv[] = 'DB_'.$db_id.'_VERSION='.$mysqlver;
   }
            // Now delete an existing folder (affects install and removal in the same way)
            @chdir($this->local_installpath);
            if(file_exists($this->local_installpath)){
   /**
    * Extract all needed files from the package
    *
    * @param $task an array containing all install related data
    * @param $sxe a SimpleXMLElement handle, holding APP-META.xml
    * @return boolean
    */
   private function prepareFiles($task, $sxe)
   {
      global $app;
      // Basically set the mapping for APS version 1.0, if not available -> newer way
      $mapping = $sxe->mapping;
      $mapping_path = $sxe->mapping['path'];
      $mapping_url = $sxe->mapping['url'];
      if(empty($mapping))
      {
         $mapping = $sxe->service->provision->{'url-mapping'}->mapping;
         $mapping_path = $sxe->service->provision->{'url-mapping'}->mapping['path'];
         $mapping_url = $sxe->service->provision->{'url-mapping'}->mapping['url'];
      }
      try
      {
         // Make sure we have a valid mapping path (at least /)
         if(empty($mapping_path)) throw new Exception('Unable to determine a mapping path');
         $this->local_installpath = $this->document_root.$this->sublocation.'/';
         // Now delete an existing folder (affects install and removal in the same way)
         @chdir($this->local_installpath);
         if(file_exists($this->local_installpath)){
            // make sure we don't delete error and stats folders
            if($this->local_installpath == $this->document_root.'/'){
               if(is_dir($this->document_root)){
                  $files = array_diff(scandir($this->document_root), array('.','..','error','stats'));
                  $files = array_diff(scandir($this->document_root), array('.', '..', 'error', 'stats'));
                  foreach($files as $file){
                     if(is_dir($this->document_root.'/'.$file)){
                        $app->file->removeDirectory($this->document_root.'/'.$file);
@@ -420,301 +452,318 @@
            mkdir($this->local_installpath, 0777, true);
         }
            if($this->handle_type == 'install')
            {
                // Now check if the needed folder is there
                if(!file_exists($this->local_installpath))
                    throw new Exception('Unable to create a new folder for the package '.$task['path']);
                // Extract all files and assign them a new owner
                if( ($this->extractZip($this->packages_dir.'/'.$task['path'], $mapping_path, $this->local_installpath) === false)
                 || ($this->extractZip($this->packages_dir.'/'.$task['path'], 'scripts', $this->local_installpath.'install_scripts/') === false) )
                {
                    // Clean already extracted data
                    exec("rm -Rf ".escapeshellarg($this->local_installpath).'*');
                    throw new Exception('Unable to extract the package '.$task['path']);
                }
                $this->processMappings($mapping, $mapping_url, $this->local_installpath);
                // Set the appropriate file owner
                $main_domain = $app->db->queryOneRecord("SELECT value FROM aps_instances_settings
                    WHERE name = 'main_domain' AND instance_id = '".$app->db->quote($task['instance_id'])."';");
                $owner_res = $app->db->queryOneRecord("SELECT system_user, system_group FROM web_domain
         if($this->handle_type == 'install')
         {
            // Now check if the needed folder is there
            if(!file_exists($this->local_installpath))
               throw new Exception('Unable to create a new folder for the package '.$task['path']);
            // Extract all files and assign them a new owner
            if( ($this->extractZip($this->packages_dir.'/'.$task['path'], $mapping_path, $this->local_installpath) === false)
               || ($this->extractZip($this->packages_dir.'/'.$task['path'], 'scripts', $this->local_installpath.'install_scripts/') === false) )
            {
               // Clean already extracted data
               exec("rm -Rf ".escapeshellarg($this->local_installpath).'*');
               throw new Exception('Unable to extract the package '.$task['path']);
            }
            $this->processMappings($mapping, $mapping_url, $this->local_installpath);
            // Set the appropriate file owner
            $main_domain = $app->db->queryOneRecord("SELECT value FROM aps_instances_settings
                    WHERE name = 'main_domain' AND instance_id = '".$app->db->quote($task['instance_id'])."';");
            $owner_res = $app->db->queryOneRecord("SELECT system_user, system_group FROM web_domain
                        WHERE domain = '".$app->db->quote($main_domain['value'])."';");
                $this->file_owner_user = $owner_res['system_user'];
                $this->file_owner_group = $owner_res['system_group'];
                exec('chown -R '.$this->file_owner_user.':'.$this->file_owner_group.' '.escapeshellarg($this->local_installpath));
            $this->file_owner_user = $owner_res['system_user'];
            $this->file_owner_group = $owner_res['system_group'];
            exec('chown -R '.$this->file_owner_user.':'.$this->file_owner_group.' '.escapeshellarg($this->local_installpath));
            //* Chown stats directory back
            if(is_dir($this->local_installpath.'stats')) {
               exec('chown -R root:root '.escapeshellarg($this->local_installpath.'stats'));
            }
            }
        }
        catch(Exception $e)
        {
            $app->dbmaster->query('UPDATE aps_instances SET instance_status = "'.INSTANCE_ERROR.'"
         }
      }
      catch(Exception $e)
      {
         $app->dbmaster->query('UPDATE aps_instances SET instance_status = "'.INSTANCE_ERROR.'"
                WHERE id = "'.$app->db->quote($task['instance_id']).'";');
            $app->log($e->getMessage(), 1);
            return false;
        }
        return true;
    }
    /**
     * Get all user config variables and set them to environment variables
     *
     * @param $task an array containing all install related data
     */
    private function prepareUserInputData($task)
    {
        global $app;
        $userdata = $app->db->queryAllRecords("SELECT name, value FROM aps_instances_settings
            WHERE instance_id = '".$app->db->quote($task['instance_id'])."';");
        if(empty($userdata)) return false;
        foreach($userdata as $data)
        {
            // Skip unnecessary data
            if($data['name'] == 'main_location'
            || $data['name'] == 'main_domain'
            || $data['name'] == 'main_database_password'
         || $data['name'] == 'main_database_name'
         || $data['name'] == 'main_database_host'
         || $data['name'] == 'main_database_login'
            || $data['name'] == 'license') continue;
            $this->putenv[] = 'SETTINGS_'.$data['name'].'='.$data['value'];
        }
    }
         $app->log($e->getMessage(), 1);
         return false;
      }
      return true;
   }
   /**
     * 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();
    * Get all user config variables and set them to environment variables
    *
    * @param $task an array containing all install related data
    */
   private function prepareUserInputData($task)
   {
      global $app;
        // 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);
      $userdata = $app->db->queryAllRecords("SELECT name, value FROM aps_instances_settings
            WHERE instance_id = '".$app->db->quote($task['instance_id'])."';");
      if(empty($userdata)) return false;
        // 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);
    }
    /**
     * The installation script should be executed
     *
     * @param $task an array containing all install related data
     * @param $sxe a SimpleXMLElement handle, holding APP-META.xml
     * @return boolean
     */
    private function doInstallation($task, $sxe)
    {
        global $app;
        try
        {
            // Check if the install directory exists
            if(!is_dir($this->local_installpath.'install_scripts/'))
                throw new Exception('The install directory '.$this->local_installpath.' is not existing');
            // Set the executable bit to the configure script
            $cfgscript = @(string)$sxe->service->provision->{'configuration-script'}['name'];
            if(!$cfgscript) $cfgscript = 'configure';
            chmod($this->local_installpath.'install_scripts/'.$cfgscript, 0755);
            // Change to the install folder (import for the exec() below!)
      foreach($userdata as $data)
      {
         // Skip unnecessary data
         if($data['name'] == 'main_location'
            || $data['name'] == 'main_domain'
            || $data['name'] == 'main_database_password'
            || $data['name'] == 'main_database_name'
            || $data['name'] == 'main_database_host'
            || $data['name'] == 'main_database_login'
            || $data['name'] == 'license') continue;
         $this->putenv[] = 'SETTINGS_'.$data['name'].'='.$data['value'];
      }
   }
   /**
    * 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);
   }
   /**
    * The installation script should be executed
    *
    * @param $task an array containing all install related data
    * @param $sxe a SimpleXMLElement handle, holding APP-META.xml
    * @return boolean
    */
   private function doInstallation($task, $sxe)
   {
      global $app;
      try
      {
         // Check if the install directory exists
         if(!is_dir($this->local_installpath.'install_scripts/'))
            throw new Exception('The install directory '.$this->local_installpath.' is not existing');
         // Set the executable bit to the configure script
         $cfgscript = @(string)$sxe->service->provision->{'configuration-script'}['name'];
         if(!$cfgscript) $cfgscript = 'configure';
         chmod($this->local_installpath.'install_scripts/'.$cfgscript, 0755);
         // Change to the install folder (import for the exec() below!)
         //exec('chown -R '.$this->file_owner_user.':'.$this->file_owner_group.' '.escapeshellarg($this->local_installpath));
            chdir($this->local_installpath.'install_scripts/');
         chdir($this->local_installpath.'install_scripts/');
         // Set the enviroment variables
         foreach($this->putenv as $var) {
            putenv($var);
         }
            $shell_retcode = true;
            $shell_ret = array();
          exec('php '.escapeshellarg($this->local_installpath.'install_scripts/'.$cfgscript).' install 2>&1', $shell_ret, $shell_retcode);
            $shell_ret = array_filter($shell_ret);
            $shell_ret_str = implode("\n", $shell_ret);
         $shell_retcode = true;
         $shell_ret = array();
         exec('php '.escapeshellarg($this->local_installpath.'install_scripts/'.$cfgscript).' install 2>&1', $shell_ret, $shell_retcode);
         $shell_ret = array_filter($shell_ret);
         $shell_ret_str = implode("\n", $shell_ret);
         // Although $shell_retcode might be 0, there can be PHP errors. Filter them:
            if(substr_count($shell_ret_str, 'Warning: ') > 0) $shell_retcode = 1;
            // If an error has occurred, the return code is != 0
            if($shell_retcode != 0) throw new Exception($shell_ret_str);
            else
            {
                // The install succeeded, chown newly created files too
                exec('chown -R '.$this->file_owner_user.':'.$this->file_owner_group.' '.escapeshellarg($this->local_installpath));
         if(substr_count($shell_ret_str, 'Warning: ') > 0) $shell_retcode = 1;
         // If an error has occurred, the return code is != 0
         if($shell_retcode != 0) throw new Exception($shell_ret_str);
         else
         {
            // The install succeeded, chown newly created files too
            exec('chown -R '.$this->file_owner_user.':'.$this->file_owner_group.' '.escapeshellarg($this->local_installpath));
            //* Chown stats directory back
            if(is_dir($this->local_installpath.'stats')) {
               exec('chown -R root:root '.escapeshellarg($this->local_installpath.'stats'));
            }
                $app->dbmaster->query('UPDATE aps_instances SET instance_status = "'.INSTANCE_SUCCESS.'"
            $app->dbmaster->query('UPDATE aps_instances SET instance_status = "'.INSTANCE_SUCCESS.'"
                    WHERE id = "'.$app->db->quote($task['instance_id']).'";');
            }
        }
        catch(Exception $e)
        {
            $app->dbmaster->query('UPDATE aps_instances SET instance_status = "'.INSTANCE_ERROR.'"
         }
      }
      catch(Exception $e)
      {
         $app->dbmaster->query('UPDATE aps_instances SET instance_status = "'.INSTANCE_ERROR.'"
                WHERE id = "'.$app->db->quote($task['instance_id']).'";');
            $app->log($e->getMessage(), 1);
            return false;
        }
        return true;
    }
    /**
     * Cleanup: Remove install scripts, remove tasks and update the database
     *
     * @param $task an array containing all install related data
     * @param $sxe a SimpleXMLElement handle, holding APP-META.xml
     */
    private function cleanup($task, $sxe)
    {
        chdir($this->local_installpath);
        exec("rm -Rf ".escapeshellarg($this->local_installpath).'install_scripts');
    }
    /**
     * The main method which performs the actual package installation
     *
     * @param $instanceid the instanceID to install
     * @param $type the type of task to perform (installation, removal)
     */
    public function installHandler($instanceid, $type)
    {
        global $app;
        // Set the given handle type, currently supported: install, delete
        if($type == 'install' || $type == 'delete') $this->handle_type = $type;
        else return false;
        // Get all instance metadata
         $app->log($e->getMessage(), 1);
         return false;
      }
      return true;
   }
   /**
    * Cleanup: Remove install scripts, remove tasks and update the database
    *
    * @param $task an array containing all install related data
    * @param $sxe a SimpleXMLElement handle, holding APP-META.xml
    */
   private function cleanup($task, $sxe)
   {
      chdir($this->local_installpath);
      exec("rm -Rf ".escapeshellarg($this->local_installpath).'install_scripts');
   }
   /**
    * The main method which performs the actual package installation
    *
    * @param $instanceid the instanceID to install
    * @param $type the type of task to perform (installation, removal)
    */
   public function installHandler($instanceid, $type)
   {
      global $app;
      // Set the given handle type, currently supported: install, delete
      if($type == 'install' || $type == 'delete') $this->handle_type = $type;
      else return false;
      // Get all instance metadata
      /*
        $task = $app->db->queryOneRecord("SELECT * FROM aps_instances AS i
            INNER JOIN aps_packages AS p ON i.package_id = p.id
        $task = $app->db->queryOneRecord("SELECT * FROM aps_instances AS i
            INNER JOIN aps_packages AS p ON i.package_id = p.id
            INNER JOIN client AS c ON i.customer_id = c.client_id
            WHERE i.id = ".$instanceid.";");
      */
      $task = $app->db->queryOneRecord("SELECT * FROM aps_instances AS i
      $task = $app->db->queryOneRecord("SELECT * FROM aps_instances AS i
            INNER JOIN aps_packages AS p ON i.package_id = p.id
            WHERE i.id = ".$instanceid.";");
        if(!$task) return false;  // formerly: throw new Exception('The InstanceID doesn\'t exist.');
        if(!isset($task['instance_id'])) $task['instance_id'] = $instanceid;
      if(!$task) return false;  // formerly: throw new Exception('The InstanceID doesn\'t exist.');
      if(!isset($task['instance_id'])) $task['instance_id'] = $instanceid;
      // Download aps package
      if(!file_exists($this->packages_dir.'/'.$task['path']) || filesize($this->packages_dir.'/'.$task['path']) == 0) {
         $ch = curl_init();
         $fh = fopen($this->packages_dir.'/'.$task['path'], 'wb');
         curl_setopt($ch, CURLOPT_FILE, $fh);
         //curl_setopt($ch, CURLOPT_HEADER, 0);
         curl_setopt($ch, CURLOPT_FILE, $fh);
         //curl_setopt($ch, CURLOPT_HEADER, 0);
         curl_setopt($ch, CURLOPT_URL, $task['package_url']);
         curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
         curl_setopt($ch, CURLOPT_TIMEOUT, 0);
         curl_setopt($ch, CURLOPT_FAILONERROR, 1);
         curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
         if(curl_exec($ch) === false) $app->log(curl_error ($ch),1);
         curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
         if(curl_exec($ch) === false) $app->log(curl_error($ch), 1);
         fclose($fh);
         curl_close($ch);
      }
      /*
      $app_to_dl[] = array('name' => $task['path'],
                            'url' => $task['package_url'],
                            'filesize' => 0,
      $app_to_dl[] = array('name' => $task['path'],
                            'url' => $task['package_url'],
                            'filesize' => 0,
                            'localtarget' => $this->packages_dir.'/'.$task['path']);
        $this->fetchFiles($app_to_dl);
      */
      // Make sure the requirements are given so that this script can execute
        $req_ret = $this->checkRequirements();
        if(!$req_ret) return false;
        $metafile = $this->getContentFromZIP($this->packages_dir.'/'.$task['path'], 'APP-META.xml');
        // Check if the meta file is existing
        if(!$metafile)
        {
            $app->dbmaster->query('UPDATE aps_instances SET instance_status = "'.INSTANCE_ERROR.'"
                WHERE id = "'.$app->db->quote($task['instance_id']).'";');
            $app->log('Unable to find the meta data file of package '.$task['path'], 1);
            return false;
        }
        // Rename namespaces and register them
        $metadata = str_replace("xmlns=", "ns=", $metafile);
        $sxe = new SimpleXMLElement($metadata);
        $namespaces = $sxe->getDocNamespaces(true);
        foreach($namespaces as $ns => $url) $sxe->registerXPathNamespace($ns, $url);
        // Setup the environment with data for the install location
        $this->prepareLocation($task);
        // Create the database if necessary
        $this->prepareDatabase($task, $sxe);
        // Unpack the install scripts from the packages
        if($this->prepareFiles($task, $sxe) && $this->handle_type == 'install')
        {
            // Setup the variables from the install script
            $this->prepareUserInputData($task);
            // Do the actual installation
            $this->doInstallation($task, $sxe);
            // Remove temporary files
            $this->cleanup($task, $sxe);
        }
        // Finally delete the instance entry + settings
        if($this->handle_type == 'delete')
        {
            $app->db->query('DELETE FROM aps_instances WHERE id = "'.$app->db->quote($task['instance_id']).'";');
            $app->db->query('DELETE FROM aps_instances_settings WHERE instance_id = "'.$app->db->quote($task['instance_id']).'";');
        }
        unset($sxe);
    }
      // Make sure the requirements are given so that this script can execute
      $req_ret = $this->checkRequirements();
      if(!$req_ret) return false;
      $metafile = $this->getContentFromZIP($this->packages_dir.'/'.$task['path'], 'APP-META.xml');
      // Check if the meta file is existing
      if(!$metafile)
      {
         $app->dbmaster->query('UPDATE aps_instances SET instance_status = "'.INSTANCE_ERROR.'"
                WHERE id = "'.$app->db->quote($task['instance_id']).'";');
         $app->log('Unable to find the meta data file of package '.$task['path'], 1);
         return false;
      }
      // Rename namespaces and register them
      $metadata = str_replace("xmlns=", "ns=", $metafile);
      $sxe = new SimpleXMLElement($metadata);
      $namespaces = $sxe->getDocNamespaces(true);
      foreach($namespaces as $ns => $url) $sxe->registerXPathNamespace($ns, $url);
      // Setup the environment with data for the install location
      $this->prepareLocation($task);
      // Create the database if necessary
      $this->prepareDatabase($task, $sxe);
      // Unpack the install scripts from the packages
      if($this->prepareFiles($task, $sxe) && $this->handle_type == 'install')
      {
         // Setup the variables from the install script
         $this->prepareUserInputData($task);
         // Do the actual installation
         $this->doInstallation($task, $sxe);
         // Remove temporary files
         $this->cleanup($task, $sxe);
      }
      // Finally delete the instance entry + settings
      if($this->handle_type == 'delete')
      {
         $app->db->query('DELETE FROM aps_instances WHERE id = "'.$app->db->quote($task['instance_id']).'";');
         $app->db->query('DELETE FROM aps_instances_settings WHERE instance_id = "'.$app->db->quote($task['instance_id']).'";');
         if ($app->dbmaster != $app->db) {
            $app->dbmaster->query('DELETE FROM aps_instances WHERE id = "'.$app->db->quote($task['instance_id']).'";');
            $app->dbmaster->query('DELETE FROM aps_instances_settings WHERE instance_id = "'.$app->db->quote($task['instance_id']).'";');
         }
      }
      unset($sxe);
   }
}
?>