(2003-2009) * @author Froxlor team (2010-) * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt * @package APS * @version $Id$ * @todo logging * run with user uid/gid * folder truncation/tar all files */ class ApsInstaller extends ApsParser { private $db = false; private $db_root = false; private $DomainPath = ''; private $Domain = ''; private $RealPath = ''; private $RootDir = ''; private $Hosts = ''; /** * constructor of class. setup some basic variables * * @param settings array with the global settings from syscp * @param db instance of the database class from syscp * @param db_root instance of the database class from syscp with root permissions */ public function __construct($settings, $db, $db_root) { $this->db = $db; $this->db_root = $db_root; $this->RootDir = dirname(dirname(__FILE__)) . '/'; $this->Hosts = $settings['system']['mysql_access_host']; } /** * main function of class which handles all */ public function InstallHandler() { chdir($this->RootDir); $result = $this->db->query('SELECT * FROM `' . TABLE_APS_TASKS . '` AS `t` INNER JOIN `' . TABLE_APS_INSTANCES . '` AS `i` ON `t`.`InstanceID` = `i`.`ID` INNER JOIN `' . TABLE_APS_PACKAGES . '` AS `p` ON `i`.`PackageID` = `p`.`ID` INNER JOIN `' . TABLE_PANEL_CUSTOMERS . '` AS `c` ON `i`.`CustomerID` = `c`.`customerid` WHERE `TASK` NOT IN (' . TASK_SYSTEM_UPDATE . ', ' . TASK_SYSTEM_DOWNLOAD . ')'); while($Row = $this->db->fetch_array($result)) { //check for existing aps xml file if(!file_exists($this->RootDir . 'packages/' . $Row['Path'] . '/APP-META.xml')) { $this->db->query('UPDATE `' . TABLE_APS_INSTANCES . '` SET `Status` = ' . INSTANCE_ERROR . ' WHERE `ID` = ' . $this->db->escape($Row['InstanceID'])); continue; } //get contents and parse them $XmlContent = file_get_contents($this->RootDir . 'packages/' . $Row['Path'] . '/APP-META.xml'); $Xml = new SimpleXMLElement($XmlContent); //check for unparseable xml data if($Xml == false) { $this->db->query('UPDATE `' . TABLE_APS_INSTANCES . '` SET `Status` = ' . INSTANCE_ERROR . ' WHERE `ID` = ' . $this->db->escape($Row['InstanceID'])); continue; } $Task = $Row['Task']; $this->DomainPath = ''; $this->Domain = ''; $this->RealPath = ''; //lock instance so installation cannot be canceled from the panel $this->db->query('UPDATE `' . TABLE_APS_INSTANCES . '` SET `Status` = ' . INSTANCE_TASK_ACTIVE . ' WHERE `ID` = ' . $this->db->escape($Row['InstanceID'])); //setup environment with data for domain/installation location self::PrepareBasics($Row); //create database if necessary and setup environment variables self::PrepareDatabase($Xml, $Row, $Task); //unpack installation scripts and package files if necessary if(self::PrepareFiles($Xml, $Row, $Task)) { //setup environment variables fetched from installation wizard self::PrepareWizardData($Xml, $Row, $Task); //run installation scripts from packages self::RunInstaller($Xml, $Row, $Task); } //remove installation scripts self::CleanupData($Xml, $Row, $Task); unset($Xml); } } /** * run the installation script and log errors if there are some * * @param xml instance of a valid xml object with a parsed APP-META.xml file * @param row current entry from the database for app to handle * @param task numeric code to specify what to do * @return success true/error false */ private function RunInstaller($Xml, $Row, $Task) { //installation if($Task == TASK_INSTALL) { //setup right path and run installation script chdir($this->RealPath . $this->DomainPath . '/install_scripts/'); $Return = array(); $ReturnStatus = 0; $Return = safe_exec('php ' . escapeshellarg($this->RealPath . $this->DomainPath . '/install_scripts/configure install'), $ReturnStatus); if($ReturnStatus != 0) { //write output of script on error into database for admin $Buffer = ''; $Count = 0; foreach($Return as $Line) { $Count+= 1; $Buffer.= $Line; if($Count != count($Return))$Buffer.= "\n"; } //FIXME error logging echo ("error : installer\n" . $Buffer . "\n"); $this->db->query('UPDATE `' . TABLE_APS_INSTANCES . '` SET `Status` = ' . INSTANCE_ERROR . ' WHERE `ID` = ' . $this->db->escape($Row['InstanceID'])); return false; } else { //installation succeeded //chown all files if installtion script has created some new files. otherwise customers cannot edit the files via ftp safe_exec('chown ' . (int)$Row['guid'] . ':' . (int)$Row['guid'] . ' -R ' . escapeshellarg($this->RealPath . $this->DomainPath . '/')); //update database $this->db->query('UPDATE `' . TABLE_APS_INSTANCES . '` SET `Status` = ' . INSTANCE_SUCCESS . ' WHERE `ID` = ' . $this->db->escape($Row['InstanceID'])); return true; } } } /** * remove installation scripts from filesystem and remove tasks and update the database * * @param xml instance of a valid xml object with a parsed APP-META.xml file * @param row current entry from the database for app to handle * @param task numeric code to specify what to do */ private function CleanupData($Xml, $Row, $Task) { chdir($this->RootDir); if($Task == TASK_INSTALL) { //cleanup installation self::UnlinkRecursive($this->RealPath . $this->DomainPath . '/install_scripts/'); //remove task $this->db->query('DELETE FROM `' . TABLE_APS_TASKS . '` WHERE `Task` = ' . TASK_INSTALL . ' AND `InstanceID` = ' . $this->db->escape($Row['InstanceID'])); } elseif($Task == TASK_REMOVE) { //FIXME cleanup installation //remove files from: $this->RealPath . $this->DomainPath . '/' //remove permissions //drop database $XmlDb = $Xml->requirements->children('http://apstandard.com/ns/1/db'); if($XmlDb->db->id) { //database management $Database = 'web' . $Row['CustomerID'] . 'aps' . $Row['InstanceID']; foreach(array_map('trim', explode(',', $this->Hosts)) as $DatabaseHost) { $this->db_root->query('REVOKE ALL PRIVILEGES ON * . * FROM `' . $this->db->escape($Database) . '`@`' . $this->db->escape($DatabaseHost) . '`'); $this->db_root->query('REVOKE ALL PRIVILEGES ON `' . $this->db->escape($Database) . '` . * FROM `' . $this->db->escape($Database) . '`@`' . $this->db->escape($DatabaseHost) . '`'); $this->db_root->query('DELETE FROM `mysql`.`user` WHERE `User` = "' . $this->db->escape($Database) . '" AND `Host` = "' . $this->db->escape($DatabaseHost) . '"'); } $this->db_root->query('DROP DATABASE IF EXISTS `' . $this->db->escape($Database) . '`'); $this->db_root->query('FLUSH PRIVILEGES'); } //remove task & delete package instance + settings $this->db->query('DELETE FROM `' . TABLE_APS_TASKS . '` WHERE `Task` = ' . TASK_REMOVE . ' AND `InstanceID` = ' . $this->db->escape($Row['InstanceID'])); $this->db->query('DELETE FROM `' . TABLE_APS_INSTANCES . '` WHERE `ID` = ' . $this->db->escape($Row['InstanceID'])); $this->db->query('DELETE FROM `' . TABLE_APS_SETTINGS . '` WHERE `InstanceID` = ' . $this->db->escape($Row['InstanceID'])); } } /** * setup all environment variables from the wizard, they're all needed by the installation script * * @param xml instance of a valid xml object with a parsed APP-META.xml file * @param row current entry from the database for app to handle * @param task numeric code to specify what to do */ private function PrepareWizardData($Xml, $Row, $Task) { //data collected by wizard //FIXME install_only parameter/reconfigure $result = $this->db->query('SELECT * FROM `' . TABLE_APS_SETTINGS . '` WHERE `InstanceID` = ' . $this->db->escape($Row['InstanceID'])); while($Row2 = $this->db->fetch_array($result)) { //skip APS internal data if($Row2['Name'] == 'main_location' || $Row2['Name'] == 'main_domain' || $Row2['Name'] == 'main_database_password' || $Row2['Name'] == 'license')continue; putenv('SETTINGS_' . $Row2['Name'] . '=' . $Row2['Value']); } } /** * extract all needed files from the aps packages * * @param xml instance of a valid xml object with a parsed APP-META.xml file * @param row current entry from the database for app to handle * @param task numeric code to specify what to do * @return success true/error false */ private function PrepareFiles($Xml, $Row, $Task) { if($Task == TASK_INSTALL) { //FIXME truncate customer directory //remove files from: $this->RealPath . $this->DomainPath . '/*' if(!file_exists($this->RealPath . $this->DomainPath . '/'))mkdir($this->RealPath . $this->DomainPath . '/', 0777, true); //extract all files and chown them to the customer guid if(self::ExtractZip($this->RootDir . 'packages/' . $Row['Path'] . '/' . $Row['Path'], $Xml->mapping['path'], $this->RealPath . $this->DomainPath . '/') == false || self::ExtractZip($this->RootDir . 'packages/' . $Row['Path'] . '/' . $Row['Path'], 'scripts', $this->RealPath . $this->DomainPath . '/install_scripts/') == false) { $this->db->query('UPDATE `' . TABLE_APS_INSTANCES . '` SET `Status` = ' . INSTANCE_ERROR . ' WHERE `ID` = ' . $this->db->escape($Row['InstanceID'])); //FIXME clean up already installed data //remove files from: $this->RealPath . $this->DomainPath . '/*' return false; } safe_exec('chown ' . (int)$Row['guid'] . ':' . (int)$Row['guid'] . ' -R ' . escapeshellarg($this->RealPath . $this->DomainPath . '/')); } else { if(self::ExtractZip($this->RootDir . 'packages/' . $Row['Path'] . '/' . $Row['Path'], 'scripts', $this->RealPath . $this->DomainPath . '/install_scripts/') == false) { $this->db->query('UPDATE `' . TABLE_APS_INSTANCES . '` SET `Status` = ' . INSTANCE_ERROR . ' WHERE `ID` = ' . $this->db->escape($Row['InstanceID'])); //clean up already installed data self::UnlinkRecursive($this->RealPath . $this->DomainPath . '/install_scripts/'); return false; } //set right file owner safe_exec('chown ' . (int)$Row['guid'] . ':' . (int)$Row['guid'] . ' -R ' . escapeshellarg($this->RealPath . $this->DomainPath . '/')); } //recursive mappings self::PrepareMappings($Xml->mapping, $Xml->mapping['url'], $this->RealPath . $this->DomainPath . '/'); return true; } /** * setup path environment variables for the installation script * * @param parentmapping instance of parsed xml file, current mapping position * @param url relative path for application specifying the current path within the mapping tree * @param path absolute path for application specifying the current path within the mapping tree */ private function PrepareMappings($ParentMapping, $Url, $Path) { //check for special PHP permissions //must be done with xpath otherwise check not possible (XML parser problem with attributes) $ParentMapping->registerXPathNamespace('p', 'http://apstandard.com/ns/1/php'); $Result = $ParentMapping->xpath('p:permissions'); if($Result[0]['writable'] == 'true') { //fixing file permissions to writeable if(is_dir($Path)) { chmod($Path, 0775); } else { chmod($Path, 0664); } } if($Result[0]['readable'] == 'false') { //fixing file permissions to non readable if(is_dir($Path)) { chmod($Path, 0333); } else { chmod($Path, 0222); } } //set environment variables $EnvVariable = str_replace("/", "_", $Url); putenv('WEB_' . $EnvVariable . '_DIR=' . $Path); //resolve deeper mappings foreach($ParentMapping->mapping as $Mapping) { //recursive check of other mappings if($Url == '/') { self::PrepareMappings($Mapping, $Url . $Mapping['url'], $Path . $Mapping['url']); } else { self::PrepareMappings($Mapping, $Url . '/' . $Mapping['url'], $Path . '/' . $Mapping['url']); } } } /** * setup domain environment variables for the installation script * * @param xml instance of a valid xml object with a parsed APP-META.xml file */ private function PrepareBasics($Row) { //domain $result = $this->db->query('SELECT * FROM `' . TABLE_APS_SETTINGS . '` WHERE `InstanceID` = ' . $this->db->escape($Row['InstanceID']) . ' AND `Name` = "main_domain"'); $Row3 = $this->db->fetch_array($result); $result2 = $this->db->query('SELECT * FROM `' . TABLE_PANEL_DOMAINS . '` WHERE `id` = ' . $this->db->escape($Row3['Value'])); $Row3 = $this->db->fetch_array($result2); $this->Domain = $Row3['domain']; $this->RealPath = $Row3['documentroot']; //location $result3 = $this->db->query('SELECT * FROM `' . TABLE_APS_SETTINGS . '` WHERE `InstanceID` = ' . $this->db->escape($Row['InstanceID']) . ' AND `Name` = "main_location"'); $Row3 = $this->db->fetch_array($result3); $this->DomainPath = $Row3['Value']; //if application is directly installed on domain remove / at the end if($this->DomainPath == '')$this->RealPath = substr($this->RealPath, 0, strlen($this->RealPath) - 1); //url environment variables putenv('BASE_URL_HOST=' . $this->Domain); putenv('BASE_URL_PATH=' . $this->DomainPath . '/'); putenv('BASE_URL_SCHEME=http'); } /** * create a database if necessary and setup environment variables * * @param xml instance of a valid xml object with a parsed APP-META.xml file * @param row current entry from the database for app to handle * @param task numeric code to specify what to do */ private function PrepareDatabase($Xml, $Row, $Task) { global $db_root; $XmlDb = $Xml->requirements->children('http://apstandard.com/ns/1/db'); if($XmlDb->db->id) { //database management $NewDatabase = 'web' . $Row['CustomerID'] . 'aps' . $Row['InstanceID']; $result = $this->db->query('SELECT * FROM `' . TABLE_APS_SETTINGS . '` WHERE `InstanceID` = ' . $this->db->escape($Row['InstanceID']) . ' AND `Name` = "main_database_password"'); $Row3 = $this->db->fetch_array($result); $DbPassword = $Row3['Value']; if($Task == TASK_INSTALL) { $this->db_root->query('DROP DATABASE IF EXISTS `' . $this->db->escape($NewDatabase) . '`'); $this->db_root->query('CREATE DATABASE IF NOT EXISTS `' . $this->db->escape($NewDatabase) . '`'); foreach(array_map('trim', explode(',', $this->Hosts)) as $DatabaseHost) { $this->db_root->query('GRANT ALL PRIVILEGES ON `' . $this->db->escape($NewDatabase) . '`.* TO `' . $this->db->escape($NewDatabase) . '`@`' . $this->db->escape($DatabaseHost) . '` IDENTIFIED BY \'password\''); $this->db_root->query('SET PASSWORD FOR `' . $this->db->escape($NewDatabase) . '`@`' . $this->db->escape($DatabaseHost) . '` = PASSWORD(\'' . $DbPassword . '\')'); } $this->db_root->query('FLUSH PRIVILEGES'); } //get first mysql access host $AccessHosts = array_map('trim', explode(',', $this->Hosts)); //environment variables putenv('DB_' . $XmlDb->db->id . '_TYPE=mysql'); putenv('DB_' . $XmlDb->db->id . '_NAME=' . $NewDatabase); putenv('DB_' . $XmlDb->db->id . '_LOGIN=' . $NewDatabase); putenv('DB_' . $XmlDb->db->id . '_PASSWORD=' . $DbPassword); putenv('DB_' . $XmlDb->db->id . '_HOST=' . $AccessHosts[0]); putenv('DB_' . $XmlDb->db->id . '_PORT=3306'); putenv('DB_' . $XmlDb->db->id . '_VERSION=' . mysql_get_server_info()); } } /** * extract complete directories from a zipfile * * @param filename path to zipfile to extract * @param directory which directory in zipfile to extract * @param destination destination directory for files to extract * @return success true/error false */ private function ExtractZip($Filename, $Directory, $Destination) { if(!file_exists($Filename))return false; //fix slash notation for correct paths if(substr($Directory, -1, 1) == '/')$Directory = substr($Directory, 0, strlen($Directory) - 1); if(substr($Destination, -1, 1) != '/')$Destination.= '/'; //open zipfile to read its contents $ZipHandle = zip_open(realpath($Filename)); if(is_resource($ZipHandle)) { while($ZipEntry = zip_read($ZipHandle)) { if(substr(zip_entry_name($ZipEntry), 0, strlen($Directory)) == $Directory) { //fix relative path from zipfile $NewPath = zip_entry_name($ZipEntry); $NewPath = substr($NewPath, strlen($Directory)); //directory if(substr($NewPath, -1, 1) == '/') { if(!file_exists($Destination . $NewPath))mkdir($Destination . $NewPath, 0777, true); } else { //files if(zip_entry_open($ZipHandle, $ZipEntry)) { // handle new directory $dir = dirname($Destination.$NewPath); if (!file_exists($dir)) { mkdir ($dir, 0777, true); } $File = fopen($Destination . $NewPath, "wb"); if($File) { while($Line = zip_entry_read($ZipEntry)) { fwrite($File, $Line); } fclose($File); } else { return false; } } } } } zip_close($ZipHandle); return true; } else { $ReturnLines = array(); $ReturnVal = - 1; //on 64 bit systems the zip functions can fail -> use exec to extract the files $ReturnLines = safe_exec('unzip -o -qq ' . escapeshellarg(realpath($Filename)) . ' ' . escapeshellarg($Directory . '/*') . ' -d ' . escapeshellarg(sys_get_temp_dir()), $ReturnVal); if($ReturnVal == 0) { //fix absolute structure of extracted data if(!file_exists($Destination))mkdir($Destination, 0777, true); safe_exec('cp -Rf ' . sys_get_temp_dir() . '/' . $Directory . '/*' . ' ' . escapeshellarg($Destination)); self::UnlinkRecursive(sys_get_temp_dir() . '/' . $Directory . '/'); return true; } else { return false; } } return false; } } ?>