From 7d4360a04e0c1d1fa20a7d8b098f36793b2b15a3 Mon Sep 17 00:00:00 2001
From: tbrehm <t.brehm@ispconfig.org>
Date: Thu, 31 May 2012 11:36:38 -0400
Subject: [PATCH] Added server part of the aps installer.
---
server/plugins-available/aps_plugin.inc.php | 108 ++++++
install/sql/ispconfig3.sql | 1
server/lib/classes/aps_base.inc.php | 109 ++++++
interface/lib/classes/aps_guicontroller.inc.php | 6
interface/lib/classes/aps_crawler.inc.php | 12
install/sql/incremental/upd_0034.sql | 1
server/mods-available/web_module.inc.php | 38 ++
interface/web/sites/aps_cron_apscrawler_if.php | 3
server/lib/classes/aps_installer.inc.php | 670 +++++++++++++++++++++++++++++++++++++++++
9 files changed, 943 insertions(+), 5 deletions(-)
diff --git a/install/sql/incremental/upd_0034.sql b/install/sql/incremental/upd_0034.sql
index ea84577..16d6a0d 100644
--- a/install/sql/incremental/upd_0034.sql
+++ b/install/sql/incremental/upd_0034.sql
@@ -46,6 +46,7 @@
`category` varchar(255) NOT NULL,
`version` varchar(20) NOT NULL,
`release` int(4) NOT NULL,
+ `package_url` TEXT NOT NULL,
`package_status` int(1) NOT NULL DEFAULT '2',
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 ;
diff --git a/install/sql/ispconfig3.sql b/install/sql/ispconfig3.sql
index 8a08f31..91a5212 100644
--- a/install/sql/ispconfig3.sql
+++ b/install/sql/ispconfig3.sql
@@ -94,6 +94,7 @@
`category` varchar(255) NOT NULL,
`version` varchar(20) NOT NULL,
`release` int(4) NOT NULL,
+ `package_url` TEXT NOT NULL,
`package_status` int(1) NOT NULL DEFAULT '2',
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 ;
diff --git a/interface/lib/classes/aps_crawler.inc.php b/interface/lib/classes/aps_crawler.inc.php
index 39375f5..e4ca565 100644
--- a/interface/lib/classes/aps_crawler.inc.php
+++ b/interface/lib/classes/aps_crawler.inc.php
@@ -34,6 +34,9 @@
class ApsCrawler extends ApsBase
{
+
+ public $app_download_url_list = array();
+
/**
* Constructor
*
@@ -290,6 +293,8 @@
$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");
+
+ $this->app_download_url_list[$app_name.'-'.$new_ver.'.app.zip'] = $app_dl;
// Skip ASP.net packages because they can't be used at all
$asp_handler = parent::getXPathValue($sxe, '//aspnet:handler');
@@ -476,12 +481,13 @@
$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)
+ 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);
@@ -515,10 +521,10 @@
".$this->db->quote($pkg_release).", ".PACKAGE_ENABLED.");");
*/
- $insert_data = "(`path`, `name`, `category`, `version`, `release`, `package_status`) VALUES
+ $insert_data = "(`path`, `name`, `category`, `version`, `release`, `package_url`, `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.");";
+ ".$this->db->quote($pkg_release).", '".$this->db->quote($this->app_download_url_list[$pkg])."', ".PACKAGE_ENABLED.");";
$this->app->db->datalogInsert('aps_packages', $insert_data, 'id');
}
diff --git a/interface/lib/classes/aps_guicontroller.inc.php b/interface/lib/classes/aps_guicontroller.inc.php
index 0b4038f..55d6db0 100644
--- a/interface/lib/classes/aps_guicontroller.inc.php
+++ b/interface/lib/classes/aps_guicontroller.inc.php
@@ -199,6 +199,8 @@
{
global $app;
+ include_once(ISPC_WEB_PATH.'/sites/tools.inc.php');
+
$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'];
@@ -256,9 +258,11 @@
if($tmp['number'] == 0) break;
}
+ $mysql_db_password = $settings['main_database_password'];
+
//* 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']."')";
+ 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', PASSWORD('$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
diff --git a/interface/web/sites/aps_cron_apscrawler_if.php b/interface/web/sites/aps_cron_apscrawler_if.php
index 32095d7..6bfa89d 100644
--- a/interface/web/sites/aps_cron_apscrawler_if.php
+++ b/interface/web/sites/aps_cron_apscrawler_if.php
@@ -32,6 +32,9 @@
//require_once('classes/class.crawler.php');
$app->load('aps_crawler');
+if(!@ini_get('allow_url_fopen')) $app->error('allow_url_fopen is not enabled');
+if(!function_exists('curl_version')) $app->error('cURL is not available');
+
$log_prefix = 'APS crawler cron: ';
$aps = new ApsCrawler($app, true); // true = Interface mode, false = Server mode
diff --git a/server/lib/classes/aps_base.inc.php b/server/lib/classes/aps_base.inc.php
new file mode 100644
index 0000000..9822cae
--- /dev/null
+++ b/server/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/server/lib/classes/aps_installer.inc.php b/server/lib/classes/aps_installer.inc.php
new file mode 100644
index 0000000..b567c81
--- /dev/null
+++ b/server/lib/classes/aps_installer.inc.php
@@ -0,0 +1,670 @@
+<?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 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 $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()
+ {
+ 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)
+ {
+ $this->app->log('Aborting execution because '.$e->getMessage());
+ 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)
+ {
+ // 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 = $this->app->db->queryOneRecord("SELECT value FROM aps_instances_settings
+ WHERE name = 'main_domain' AND instance_id = '".$this->db->quote($task['instance_id'])."';");
+ $this->domain = $main_domain['value'];
+
+ // Get the document root
+ $domain_res = $this->app->db->queryOneRecord("SELECT document_root FROM web_domain
+ WHERE domain = '".$this->db->quote($this->domain)."';");
+ $this->document_root = $domain_res['document_root'];
+
+ // Get the sub location
+ $location_res = $this->app->dbmaster->queryOneRecord("SELECT value FROM aps_instances_settings
+ WHERE name = 'main_location' AND instance_id = '".$this->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:
+ $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)
+ {
+ $db_id = parent::getXPathValue($sxe, '//db:id');
+ if(empty($db_id)) return; // No database needed
+
+ /*
+ // 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 = $this->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
+ $this->app->uses('getconf');
+ $global_config = $this->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 = $this->app->dbmaster->queryOneRecord("SELECT Value FROM aps_instances_settings
+ WHERE Name = 'main_database_password' AND InstanceID = '".$this->db->quote($task['InstanceID'])."';");
+ $newdb_pw = $dbpw_res['Value'];
+
+ // In any case delete an existing database (install and removal procedure)
+ $this->db->query('DROP DATABASE IF EXISTS `'.$this->db->quote($this->newdb_name).'`;');
+ // Delete an already existing database with this name
+ $this->app->dbmaster->query("DELETE FROM web_database WHERE database_name = '".$this->db->quote($this->newdb_name)."';");
+
+
+ // Create the new database and assign it to a user
+ if($this->handle_type == 'install')
+ {
+ $this->db->query('CREATE DATABASE IF NOT EXISTS `'.$this->db->quote($this->newdb_name).'`;');
+ $this->db->query('GRANT ALL PRIVILEGES ON '.$this->db->quote($this->newdb_name).'.* TO '.$this->db->quote($this->newdb_user).'@'.$this->db->quote($this->dbhost).' IDENTIFIED BY \'password\';');
+ $this->db->query('SET PASSWORD FOR '.$this->db->quote($this->newdb_user).'@'.$this->db->quote($this->dbhost).' = PASSWORD(\''.$newdb_pw.'\');');
+ $this->db->query('FLUSH PRIVILEGES;');
+
+ // Add the new database to the customer databases
+ // Assumes: charset = utf8
+ $this->app->dbmaster->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'].'", '.$this->db->quote($serverid).', "mysql", "'.$this->db->quote($this->newdb_name).'",
+ "'.$this->db->quote($this->newdb_user).'", "'.$this->db->quote($newdb_pw).'", "utf8", "n", "", "y");');
+ }
+ */
+
+ $mysqlver_res = $this->app->db->queryOneRecord('SELECT VERSION() as ver;');
+ $mysqlver = $mysqlver_res['ver'];
+
+ $tmp = $this->app->dbmaster->queryOneRecord("SELECT value FROM aps_instances_settings WHERE name = 'main_database_password' AND instance_id = '".$this->db->quote($task['instance_id'])."';");
+ $newdb_pw = $tmp['value'];
+
+ $tmp = $this->app->dbmaster->queryOneRecord("SELECT value FROM aps_instances_settings WHERE name = 'main_database_host' AND instance_id = '".$this->db->quote($task['instance_id'])."';");
+ $newdb_host = $tmp['value'];
+
+ $tmp = $this->app->dbmaster->queryOneRecord("SELECT value FROM aps_instances_settings WHERE name = 'main_database_name' AND instance_id = '".$this->db->quote($task['instance_id'])."';");
+ $newdb_name = $tmp['value'];
+
+ $tmp = $this->app->dbmaster->queryOneRecord("SELECT value FROM aps_instances_settings WHERE name = 'main_database_login' AND instance_id = '".$this->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)
+ {
+ // 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)) exec("rm -Rf ".escapeshellarg($this->local_installpath).'*');
+ else 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 = $this->app->db->queryOneRecord("SELECT value FROM aps_instances_settings
+ WHERE name = 'main_domain' AND instance_id = '".$this->db->quote($task['instance_id'])."';");
+ $owner_res = $this->db->queryOneRecord("SELECT system_user, system_group FROM web_domain
+ WHERE domain = '".$this->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));
+ }
+ }
+ catch(Exception $e)
+ {
+ $this->app->dbmaster->query('UPDATE aps_instances SET instance_status = "'.INSTANCE_ERROR.'"
+ WHERE id = "'.$this->db->quote($task['instance_id']).'";');
+ $this->app->log($e->getMessage());
+ 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)
+ {
+ $userdata = $this->app->dbmaster->queryAllRecords("SELECT name, value FROM aps_instances_settings
+ WHERE instance_id = '".$this->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'];
+ }
+ }
+
+ /**
+ * 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)
+ {
+ 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/');
+
+ // 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);
+
+ // 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));
+
+ $this->app->dbmaster->query('UPDATE aps_instances SET instance_status = "'.INSTANCE_SUCCESS.'"
+ WHERE id = "'.$this->db->quote($task['instance_id']).'";');
+ }
+ }
+ catch(Exception $e)
+ {
+ $this->app->dbmaster->query('UPDATE aps_instances SET instance_status = "'.INSTANCE_ERROR.'"
+ WHERE id = "'.$this->db->quote($task['instance_id']).'";');
+ $this->app->log($e->getMessage());
+ 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)
+ {
+ // 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 = $this->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.";");
+ 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'])) {
+ $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_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) $this->app->log(curl_error ($ch),LOGLEVEL_DEBUG);
+ fclose($fh);
+ curl_close($ch);
+ }
+
+ /*
+ $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)
+ {
+ $this->app->dbmaster->query('UPDATE aps_instances SET instance_status = "'.INSTANCE_ERROR.'"
+ WHERE id = "'.$this->db->quote($task['instance_id']).'";');
+ $this->app->log('Unable to find the meta data file of package '.$task['path']);
+ 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')
+ {
+ $this->app->dbmaster->query('DELETE FROM aps_instances WHERE id = "'.$this->db->quote($task['instance_id']).'";');
+ $this->app->dbmaster->query('DELETE FROM aps_instances_settings WHERE instance_id = "'.$this->db->quote($task['instance_id']).'";');
+ }
+
+ unset($sxe);
+ }
+}
+?>
\ No newline at end of file
diff --git a/server/mods-available/web_module.inc.php b/server/mods-available/web_module.inc.php
index 8d5681a..dd7aba0 100644
--- a/server/mods-available/web_module.inc.php
+++ b/server/mods-available/web_module.inc.php
@@ -52,7 +52,19 @@
'web_folder_user_delete',
'web_backup_insert',
'web_backup_update',
- 'web_backup_delete');
+ 'web_backup_delete',
+ 'aps_instance_insert',
+ 'aps_instance_update',
+ 'aps_instance_delete',
+ 'aps_instance_setting_insert',
+ 'aps_instance_setting_update',
+ 'aps_instance_setting_delete',
+ 'aps_package_insert',
+ 'aps_package_update',
+ 'aps_package_delete',
+ 'aps_setting_insert',
+ 'aps_setting_update',
+ 'aps_setting_delete');
//* This function is called during ispconfig installation to determine
// if a symlink shall be created for this plugin.
@@ -98,6 +110,10 @@
$app->modules->registerTableHook('web_folder','web_module','process');
$app->modules->registerTableHook('web_folder_user','web_module','process');
$app->modules->registerTableHook('web_backup','web_module','process');
+ $app->modules->registerTableHook('aps_instances','web_module','process');
+ $app->modules->registerTableHook('aps_instances_settings','web_module','process');
+ $app->modules->registerTableHook('aps_packages','web_module','process');
+ $app->modules->registerTableHook('aps_settings','web_module','process');
// Register service
$app->services->registerService('httpd','web_module','restartHttpd');
@@ -149,6 +165,26 @@
if($action == 'u') $app->plugins->raiseEvent('web_backup_update',$data);
if($action == 'd') $app->plugins->raiseEvent('web_backup_delete',$data);
break;
+ case 'aps_instances':
+ if($action == 'i') $app->plugins->raiseEvent('aps_instance_insert',$data);
+ if($action == 'u') $app->plugins->raiseEvent('aps_instance_update',$data);
+ if($action == 'd') $app->plugins->raiseEvent('aps_instance_delete',$data);
+ break;
+ case 'aps_instances_settings':
+ if($action == 'i') $app->plugins->raiseEvent('aps_instance_setting_insert',$data);
+ if($action == 'u') $app->plugins->raiseEvent('aps_instance_setting_update',$data);
+ if($action == 'd') $app->plugins->raiseEvent('aps_instance_setting_delete',$data);
+ break;
+ case 'aps_packages':
+ if($action == 'i') $app->plugins->raiseEvent('aps_package_insert',$data);
+ if($action == 'u') $app->plugins->raiseEvent('aps_package_update',$data);
+ if($action == 'd') $app->plugins->raiseEvent('aps_package_delete',$data);
+ break;
+ case 'aps_settings':
+ if($action == 'i') $app->plugins->raiseEvent('aps_setting_insert',$data);
+ if($action == 'u') $app->plugins->raiseEvent('aps_setting_update',$data);
+ if($action == 'd') $app->plugins->raiseEvent('aps_setting_delete',$data);
+ break;
} // end switch
} // end function
diff --git a/server/plugins-available/aps_plugin.inc.php b/server/plugins-available/aps_plugin.inc.php
new file mode 100644
index 0000000..26ae9be
--- /dev/null
+++ b/server/plugins-available/aps_plugin.inc.php
@@ -0,0 +1,108 @@
+<?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(ISPC_ROOT_PATH.'/lib/classes/aps_installer.inc.php');
+//require_once(ISPC_ROOT_PATH.'/lib/classes/class.installer.php');
+
+class aps_plugin
+{
+ public $plugin_name = 'aps_plugin';
+ public $class_name = 'aps_plugin';
+
+ //* This function is called during ispconfig installation to determine
+ // if a symlink shall be created for this plugin.
+ function onInstall() {
+ global $conf;
+
+ if($conf['services']['web'] == true) {
+ return true;
+ } else {
+ return false;
+ }
+
+ }
+
+ /**
+ * This method gets called when the plugin is loaded
+ */
+ public function onLoad()
+ {
+ global $app;
+
+ // Register the available events
+ $app->plugins->registerEvent('aps_instance_install', $this->plugin_name, 'install');
+ $app->plugins->registerEvent('aps_instance_update', $this->plugin_name, 'install');
+ $app->plugins->registerEvent('aps_instance_delete', $this->plugin_name, 'delete');
+ }
+
+ /**
+ * (Re-)install a package
+ */
+ public function install($event_name, $data)
+ {
+ global $app, $conf;
+
+ $app->log("Starting APS install",LOGLEVEL_DEBUG);
+ if(!isset($data['new']['id'])) return false;
+ $instanceid = $data['new']['id'];
+
+ if($data['new']['instance_status'] == INSTANCE_INSTALL) {
+ $aps = new ApsInstaller($app);
+ $app->log("Running installHandler",LOGLEVEL_DEBUG);
+ $aps->installHandler($instanceid, 'install');
+ }
+ }
+
+ /**
+ * Update an existing instance (currently unused)
+ */
+ /*
+ public function update($event_name, $data)
+ {
+ }
+ */
+
+ /**
+ * Uninstall an instance
+ */
+ public function delete($event_name, $data)
+ {
+ global $app, $conf;
+
+ if(!isset($data['new']['id'])) return false;
+ $instanceid = $data['new']['id'];
+
+ if($data['new']['instance_status'] == INSTANCE_REMOVE) {
+ $aps = new ApsInstaller($app);
+ $aps->installHandler($instanceid, 'install');
+ }
+ }
+}
+?>
\ No newline at end of file
--
Gitblit v1.9.1