Files
Froxlor/lib/classes/aps/class.ApsInstaller.php
Michael Kaufmann (d00p) cb128e5020 - implemented APS-1.2 standard
2010-06-14 08:09:17 +00:00

635 lines
20 KiB
PHP

<?php
/**
* Implementation of the Application Packaging Standard from SwSoft/Parallels
* http://apsstandard.com
*
* This file is part of the Froxlor project.
* Copyright (c) 2003-2009 the SysCP Team (see authors).
* Copyright (c) 2010 the Froxlor Team (see authors).
*
* For the full copyright and license information, please view the COPYING
* file that was distributed with this source code. You can also view the
* COPYING file online at http://files.froxlor.org/misc/COPYING.txt
*
* @copyright (c) the authors
* @author Florian Lippert <flo@syscp.org> (2003-2009)
* @author Froxlor team <team@froxlor.org> (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 = '';
private $aps_version = '1.0';
/**
* 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(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);
$this->aps_version = isset($Xml->attributes()->version) ? (string)$Xml->attributes()->version : '1.0';
//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
if(!is_dir($this->RealPath . $this->DomainPath . '/install_scripts/'))
{
echo 'Directory: '. $this->RealPath . $this->DomainPath . '/install_scripts/ does not exist';
return;
}
chdir($this->RealPath . $this->DomainPath . '/install_scripts/');
$Return = array();
$ReturnStatus = 0;
// make configure-script executable
if($this->aps_version != '1.0')
{
$scriptname = (string)$Xml->service->provision->{'configuration-script'}['name'];
} else {
$scriptname = 'configure';
}
chmod($this->RealPath . $this->DomainPath . '/install_scripts/'.$scriptname, 0755);
$Return = safe_exec('php ' . escapeshellarg($this->RealPath . $this->DomainPath . '/install_scripts/'.$scriptname) . ' 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
if ($this->aps_version == '1.0')
{
// the good ole way
$XmlDb = $Xml->requirements->children('http://apstandard.com/ns/1/db');
}
else
{
// since 1.1
$Xml->registerXPathNamespace('db', 'http://apstandard.com/ns/1/db');
$XmlDb = new DynamicProperties;
$XmlDb->db->id = getXPathValue($Xml, '//db:id');
}
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($this->aps_version != '1.0')
{
$mapping = $Xml->service->provision->{'url-mapping'}->mapping;
$mapping_path = $Xml->service->provision->{'url-mapping'}->mapping['path'];
$mapping_url = $Xml->service->provision->{'url-mapping'}->mapping['url'];
}
else
{
$mapping = $Xml->mapping;
$mapping_path = $Xml->mapping['path'];
$mapping_url = $Xml->mapping['url'];
}
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'], $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($mapping, $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)
if($ParentMapping && $ParentMapping !== null)
{
$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
if($ParentMapping && $ParentMapping !== null)
{
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 ($this->aps_version == '1.0')
{
// the good ole way
$XmlDb = $Xml->requirements->children('http://apstandard.com/ns/1/db');
}
else
{
// since 1.1
$Xml->registerXPathNamespace('db', 'http://apstandard.com/ns/1/db');
$XmlDb = new DynamicProperties;
$XmlDb->db->id = getXPathValue($Xml, '//db:id');
}
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;
}
}
?>