From 7d4360a04e0c1d1fa20a7d8b098f36793b2b15a3 Mon Sep 17 00:00:00 2001 From: tbrehm Date: Thu, 31 May 2012 15:36:38 +0000 Subject: [PATCH] Added server part of the aps installer. --- install/sql/incremental/upd_0034.sql | 1 + install/sql/ispconfig3.sql | 1 + interface/lib/classes/aps_crawler.inc.php | 12 +- .../lib/classes/aps_guicontroller.inc.php | 6 +- .../web/sites/aps_cron_apscrawler_if.php | 3 + server/lib/classes/aps_base.inc.php | 109 +++ server/lib/classes/aps_installer.inc.php | 670 ++++++++++++++++++ server/mods-available/web_module.inc.php | 38 +- server/plugins-available/aps_plugin.inc.php | 108 +++ 9 files changed, 943 insertions(+), 5 deletions(-) create mode 100644 server/lib/classes/aps_base.inc.php create mode 100644 server/lib/classes/aps_installer.inc.php create mode 100644 server/plugins-available/aps_plugin.inc.php diff --git a/install/sql/incremental/upd_0034.sql b/install/sql/incremental/upd_0034.sql index ea8457700..16d6a0dd2 100644 --- a/install/sql/incremental/upd_0034.sql +++ b/install/sql/incremental/upd_0034.sql @@ -46,6 +46,7 @@ CREATE TABLE IF NOT EXISTS `aps_packages` ( `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 8a08f3164..91a521252 100644 --- a/install/sql/ispconfig3.sql +++ b/install/sql/ispconfig3.sql @@ -94,6 +94,7 @@ CREATE TABLE IF NOT EXISTS `aps_packages` ( `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 39375f57b..e4ca565d3 100644 --- a/interface/lib/classes/aps_crawler.inc.php +++ b/interface/lib/classes/aps_crawler.inc.php @@ -34,6 +34,9 @@ require_once('aps_base.inc.php'); class ApsCrawler extends ApsBase { + + public $app_download_url_list = array(); + /** * Constructor * @@ -290,6 +293,8 @@ class ApsCrawler extends ApsBase $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 @@ class ApsCrawler extends ApsBase $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 @@ class ApsCrawler extends ApsBase ".$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 0b4038fbd..55d6db048 100644 --- a/interface/lib/classes/aps_guicontroller.inc.php +++ b/interface/lib/classes/aps_guicontroller.inc.php @@ -199,6 +199,8 @@ class ApsGUIController extends ApsBase { 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 @@ class ApsGUIController extends ApsBase 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 32095d79d..6bfa89d6d 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('../../lib/app.inc.php'); //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 000000000..9822caeaa --- /dev/null +++ b/server/lib/classes/aps_base.inc.php @@ -0,0 +1,109 @@ +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 000000000..b567c8174 --- /dev/null +++ b/server/lib/classes/aps_installer.inc.php @@ -0,0 +1,670 @@ +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 8d5681a47..dd7aba083 100644 --- a/server/mods-available/web_module.inc.php +++ b/server/mods-available/web_module.inc.php @@ -52,7 +52,19 @@ class web_module { '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 @@ class web_module { $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 @@ class web_module { 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 000000000..26ae9bede --- /dev/null +++ b/server/plugins-available/aps_plugin.inc.php @@ -0,0 +1,108 @@ +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 -- GitLab