Commit 42a34df4 authored by Marius Burkard's avatar Marius Burkard

- installer class for addons

- do not install/update addons during install/update of ISPconfig. This only leads to lots of trouble
parent e35182f6
This empty directory is needed by ISPConfig.
......@@ -145,46 +145,6 @@ if(is_file('dist/lib/'.$dist['baseid'].'.lib.php')) include_once 'dist/lib/'.$di
include_once 'dist/lib/'.$dist['id'].'.lib.php';
include_once 'dist/conf/'.$dist['confid'].'.conf.php';
//** Include addon lib config files
if(is_dir('dist/lib.d')) {
// scheme is: <addon-name>.<distconfid>.conf.php
if(($dir = opendir('dist/lib.d'))) {
while(false !== ($cur = readdir($dir))) {
$curpath = 'dist/lib.d/' . $cur;
if(strpos($curpath, '..') !== false
|| !is_file($curpath)
|| !preg_match('/\.(?:' . preg_quote($dist['id'], '/') . '|' . preg_quote($dist['baseid'], '/') . ')\.lib\.php$/', $cur)) {
// invalid entry or entry not for current distribution
continue;
}
// valid file name and either generic or for current distribution
include_once $curpath;
}
closedir($dir);
}
}
//** Include addon dist config files
if(is_dir('dist/conf.d')) {
// scheme is: <addon-name>.<distconfid>.conf.php
if(($dir = opendir('dist/conf.d'))) {
while(false !== ($cur = readdir($dir))) {
$curpath = 'dist/conf.d/' . $cur;
if(strpos($curpath, '..') !== false
|| !is_file($curpath)
|| !preg_match('/\.' . preg_quote($dist['confid'], '/') . '\.conf\.php$/', $cur)) {
// invalid entry or entry not for current distribution
continue;
}
// valid file name and either generic or for current distribution
include_once $curpath;
}
closedir($dir);
}
}
//****************************************************************************************************
//** Installer Interface
//****************************************************************************************************
......
<?php
/**
* Base class for app installer
* This is a stripped down class with only the event method. The full class is only used in /server/lib/classes
*
* @author Marius Burkard
*/
class ispconfig_addon_installer_base {
protected $addon_ident;
public function __construct() {
$this->addon_ident = preg_replace('/_addon_installer$/', '', get_called_class());
}
public function onRaisedInstallerEvent($event_name, $data) {
}
}
......@@ -28,6 +28,8 @@ 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 realpath(dirname(__FILE__)) . '/classes/ispconfig_addon_installer.inc.php';
class installer_base {
var $wb = array();
......@@ -397,8 +399,6 @@ class installer_base {
$this->db->query($sql, $conf['hostname'], $mail_server_enabled, $web_server_enabled, $dns_server_enabled, $file_server_enabled, $db_server_enabled, $server_ini_content, $current_db_version, $proxy_server_enabled, $firewall_server_enabled);
$conf['server_id'] = $this->db->insertID();
}
}
public function detect_ips(){
......@@ -2183,7 +2183,7 @@ class installer_base {
// TODO: Implement a selector which modules and plugins shall be enabled.
$dir = $install_dir.'/server/mods-available/';
if (is_dir($dir)) {
if ($dh = opendir($dir)) {
if (($dh = opendir($dir))) {
while (($file = readdir($dh)) !== false) {
if($file != '.' && $file != '..' && substr($file, -8, 8) == '.inc.php') {
include_once $install_dir.'/server/mods-available/'.$file;
......@@ -2210,7 +2210,7 @@ class installer_base {
$dir = $install_dir.'/server/plugins-available/';
if (is_dir($dir)) {
if ($dh = opendir($dir)) {
if (($dh = opendir($dir))) {
while (($file = readdir($dh)) !== false) {
if($conf['apache']['installed'] == true && $file == 'nginx_plugin.inc.php') continue;
if($conf['nginx']['installed'] == true && $file == 'apache2_plugin.inc.php') continue;
......@@ -2828,7 +2828,7 @@ class installer_base {
}
private function loadAddonClasses($path) {
$libpath = $conf['ispconfig_install_dir'] . '/addons';
$libpath = $path;
if(($dir = opendir($libpath))) {
while(false !== ($cur = readdir($dir))) {
if($cur === '.' || $cur === '..' || strpos($cur, '..') !== false || !is_dir($libpath . '/' . $cur)) {
......@@ -2867,15 +2867,12 @@ class installer_base {
if(is_null($this->addon_classes)) {
// load addon libs
$this->addon_classes = array();
$addonpath = $conf['ispconfig_install_dir'] . '/addons';
$this->loadAddonClasses($addonpath);
// check for addon libs in install dir
$addonpath = realpath(dirname(__FILE__) . '/..') . '/addons';
$addonpath = $conf['ispconfig_install_dir'] . '/addons';
$this->loadAddonClasses($addonpath);
}
$call_method = 'onRaisedEvent';
$call_method = 'onRaisedInstallerEvent';
reset($this->addon_classes);
foreach($this->addon_classes as $cl) {
if(method_exists($cl, $call_method)) {
......
......@@ -142,3 +142,12 @@ UPDATE `spamfilter_policy` SET `rspamd_spam_kill_level` = '8.00' WHERE id = 6;
UPDATE `spamfilter_policy` SET `rspamd_spam_kill_level` = '20.00' WHERE id = 7;
-- end of rspamd
CREATE TABLE IF NOT EXISTS `addons` (
`addon_id` int(11) NOT NULL AUTO_INCREMENT,
`addon_ident` VARCHAR(100) NOT NULL DEFAULT '',
`addon_version` VARCHAR(20) NOT NULL DEFAULT '',
`addon_name` VARCHAR(255) NOT NULL DEFAULT '',
`db_version` INT(6) NOT NULL DEFAULT '0',
PRIMARY KEY (`addon_id`),
UNIQUE KEY `ident` (`addon_ident`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ;
......@@ -53,6 +53,22 @@ SET FOREIGN_KEY_CHECKS = 0;
-- --------------------------------------------------------
-- --------------------------------------------------------
--
-- Table structure for table `addons`
--
CREATE TABLE IF NOT EXISTS `addons` (
`addon_id` int(11) NOT NULL AUTO_INCREMENT,
`addon_ident` VARCHAR(100) NOT NULL DEFAULT '',
`addon_version` VARCHAR(20) NOT NULL DEFAULT '',
`addon_name` VARCHAR(255) NOT NULL DEFAULT '',
`db_version` INT(6) NOT NULL DEFAULT '0',
PRIMARY KEY (`addon_id`),
UNIQUE KEY `ident` (`addon_ident`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ;
-- --------------------------------------------------------
--
-- Table structure for table `aps_instances`
--
......
......@@ -148,45 +148,11 @@ if(is_file('dist/lib/'.$dist['baseid'].'.lib.php')) include_once 'dist/lib/'.$di
include_once 'dist/lib/'.$dist['id'].'.lib.php';
include_once 'dist/conf/'.$dist['confid'].'.conf.php';
//** Include addon lib config files
if(is_dir('dist/lib.d')) {
// scheme is: <addon-name>.<distconfid>.conf.php
if(($dir = opendir('dist/lib.d'))) {
while(false !== ($cur = readdir($dir))) {
$curpath = 'dist/lib.d/' . $cur;
if(strpos($curpath, '..') !== false
|| !is_file($curpath)
|| !preg_match('/\.(?:' . preg_quote($dist['id'], '/') . '|' . preg_quote($dist['baseid'], '/') . ')\.lib\.php$/', $cur)) {
// invalid entry or entry not for current distribution
continue;
}
// valid file name and either generic or for current distribution
include_once $curpath;
}
closedir($dir);
}
}
$inst = new installer();
if (!$inst->get_php_version()) die('ISPConfig requieres PHP '.$inst->min_php."\n");
$inst->is_update = true;
//** Include addon dist config files
if(is_dir('dist/conf.d')) {
// scheme is: <addon-name>.<distconfid>.conf.php
if(($dir = opendir('dist/conf.d'))) {
while(false !== ($cur = readdir($dir))) {
$curpath = 'dist/conf.d/' . $cur;
if(strpos($curpath, '..') !== false
|| !is_file($curpath)
|| !preg_match('/\.' . preg_quote($dist['confid'], '/') . '\.conf\.php$/', $cur)) {
// invalid entry or entry not for current distribution
continue;
}
// valid file name and either generic or for current distribution
include_once $curpath;
}
closedir($dir);
}
}
$inst->raiseEvent('set_dist_config', $dist);
//** tRNG dependencies
$conf['tRNG']='';
......@@ -228,10 +194,6 @@ if(!$conf['mysql']['ip'] = gethostbyname($conf['mysql']['host'])) die('Unable to
$conf['server_id'] = intval($conf_old["server_id"]);
$conf['ispconfig_log_priority'] = $conf_old["log_priority"];
$inst = new installer();
if (!$inst->get_php_version()) die('ISPConfig requieres PHP '.$inst->min_php."\n");
$inst->is_update = true;
echo "This application will update ISPConfig 3 on your server.\n\n";
//* Make a backup before we start the update
......
......@@ -599,6 +599,19 @@ class page_action extends tform_actions {
}
$msg .= '<br>';
}
$entries = $app->plugin->raiseEvent('tools:resync:get_resync_entries', $this->dataRecord, true);
if(is_array($entries) && !empty($entries)) {
foreach($entries as $entry) {
if(!isset($entry['db_table']) || !isset($entry['db_table']) || !isset($entry['db_table']) || !isset($entry['db_table']) || !isset($entry['db_table']) || !isset($entry['db_table'])) {
continue;
}
if(!isset($entry['active'])) {
$entry['active'] = true;
}
$msg .= $this->do_resync($entry['db_table'], $entry['index_field'], $entry['server_type'], $entry['server_id'], $entry['msg_field'], $entry['wordbook'], $entry['active']);
}
}
echo $msg;
} //* end onSumbmit
......
......@@ -122,6 +122,8 @@
<div class="col-sm-3"><select name="firewall_server_id" id="firewall_server_id" class="form-control">{tmpl_var name='firewall_server_id'}</select></div>
</div>
</tmpl_if>
{tmpl_hook name="end_form"}
<div class="form-group">
<label for="resync_client" class="col-sm-2 control-label control-label-plain">{tmpl_var name="resync_client_txt"}</label>
......
<?php
/**
* addon installer
*
* @author Marius Burkard
*/
class addon_installer {
private function extractPackage($package_file) {
global $app;
$ret = null;
$retval = 0;
$cmd = 'which unzip';
$tmp = explode("\n", exec($cmd, $ret, $retval));
if($retval != 0) {
throw new AddonInstallerException('unzip tool not found.');
}
$unzip = reset($tmp);
unset($tmp);
if(!$unzip) {
throw new AddonInstallerException('unzip tool not found.');
}
$temp_dir = $app->system->tempdir(sys_get_temp_dir(), 'addon_', 0700);
if(!$temp_dir) {
throw new AddonInstallerException('Could not create temp dir.');
}
$ret = null;
$retval = 0;
$cmd = $unzip . ' -d ' . escapeshellarg($temp_dir) . ' ' . escapeshellarg($package_file);
exec($cmd, $ret, $retval);
if($retval != 0) {
throw new AddonInstallerException('Package extraction failed.');
}
return $temp_dir;
}
/**
* @param string $path
* @return string
* @throws AddonInstallerValidationException
*/
private function validatePackage($path) {
if(!is_dir($path)) {
throw new AddonInstallerValidationException('Invalid path.');
}
$ini_file = $path . '/addon.ini';
if(!is_file($ini_file)) {
throw new AddonInstallerValidationException('Addon ini file missing.');
}
$ini = parse_ini_file($ini_file, true);
if(!$ini || !isset($ini['addon'])) {
throw new AddonInstallerValidationException('Ini file is missing addon section.');
}
$addon = $ini['addon'];
if(!isset($addon['ident']) || !isset($addon['name']) || !isset($addon['version'])) {
throw new AddonInstallerValidationException('Ini file is missing addon ident/name/version.');
}
$class_file = $path . '/' . $addon['ident'] . '.addon.php';
if(!is_file($class_file)) {
throw new AddonInstallerValidationException('Package is missing main addon class.');
}
if(isset($ini['ispconfig']['version.min']) && $ini['ispconfig']['version.min'] && version_compare($ini['ispconfig']['version.min'], ISPC_APP_VERSION, '>')) {
throw new AddonInstallerValidationException('Addon requires at least ISPConfig version ' . $ini['ispconfig']['version.min'] . '.');
} elseif(isset($ini['ispconfig']['version.max']) && $ini['ispconfig']['version.max'] && version_compare($ini['ispconfig']['version.min'], ISPC_APP_VERSION, '<')) {
throw new AddonInstallerValidationException('Addon allows at max ISPConfig version ' . $ini['ispconfig']['version.max'] . '.');
}
$addon['class_file'] = $class_file;
$addon['class_name'] = substr(basename($class_file), 0, -10) . '_addon_installer';
return $addon;
}
private function getInstalledAddonVersion($ident) {
global $app, $conf;
$file_version = false;
$db_version = false;
$addon_path = realpath($conf['rootpath'] . '/..') . '/addons';
// check for previous version
if(is_dir($addon_path . '/' . $ident) && is_file($addon_path . '/' . $ident . '/addon.ini')) {
$addon = parse_ini_file($addon_path . '/' . $ident . '/addon.ini', true);
if(!$addon || !isset($addon['version']) || !isset($addon['ident']) || $addon['ident'] != $ident) {
throw new AddonInstallerException('Installed app found but it is invalid.');
}
$file_version = $addon['version'];
}
$check = $app->db->queryOneRecord('SELECT `addon_version` FROM `addons` WHERE `addon_ident` = ?', $ident);
if($check && $check['addon_version']) {
$db_version = $check['addon_version'];
}
if(!$file_version && !$db_version) {
return false;
} elseif($file_version != $db_version) {
throw new AddonInstallerException('Addon version mismatch in database (' . $db_version . ') and file system (' . $file_version . ').');
}
return $file_version;
}
/**
* @param string $package_file Full path
* @param boolean $force true if previous addon with same or higher version should be overwritten
* @throws AddonInstallerException
* @throws AddonInstallerValidationException
*/
public function installAddon($package_file, $force = false) {
if(!is_file($package_file)) {
throw new AddonInstallerException('Package file not found.');
} elseif(substr($package_file, -4) !== '.pkg') {
throw new AddonInstallerException('Invalid package file.');
}
$tmp_dir = $this->extractPackage($package_file);
if(!$tmp_dir) {
// extracting failed
throw new AddonInstallerException('Package extraction failed.');
}
$addon = $this->validatePackage($tmp_dir);
if(!$addon) {
throw new AddonInstallerException('Package validation failed.');
}
$is_update = false;
$previous = $this->getInstalledAddonVersion($addon['ident']);
if($previous !== false) {
// this is an update
if(version_compare($previous, $addon['version'], '>') && $force !== true) {
throw new AddonInstallerException('Installed version is newer than the one to install.');
} elseif(version_compare($previous, $addon['version'], '=') && $force !== true) {
throw new AddonInstallerException('Installed version is the same as the one to install.');
}
$is_update = true;
}
include $addon['class_file'];
if(!class_exists($addon['class_name'])) {
throw new AddonInstallerException('Could not find main class in addon file.');
}
$class_name = $addon['class_name'];
/* @var $inst ispconfig_addon_installer_base */
$inst = new $class_name();
$inst->setAddonName($addon['name']);
$inst->setAddonIdent($addon['ident']);
$inst->setAddonVersion($addon['version']);
$inst->setAddonTempDir($tmp_dir);
if($is_update === true) {
$inst->onBeforeUpdate();
$inst->onUpdate();
$inst->onAfterUpdate();
} else {
$inst->onBeforeInstall();
$inst->onInstall();
$inst->onAfterInstall();
}
return true;
}
}
......@@ -297,6 +297,12 @@ class functions {
}
}
}
$tmp_ips = $app->plugins->raiseAction('get_server_ips', 0, true);
if(is_array($tmp_ips) && !empty($tmp_ips)) {
$ips = array_merge($ips, $tmp_ips);
}
$ips = array_unique($ips);
sort($ips, SORT_NUMERIC);
......
<?php
/**
* Base class for app installer
*
* @author Marius Burkard
*/
class ispconfig_addon_installer_base {
protected $addon_name;
protected $addon_ident;
protected $addon_version;
protected $temp_dir;
public function __construct() {
$this->addon_ident = preg_replace('/_addon_installer$/', '', get_called_class());
}
public function setAddonName($name) {
$this->addon_name = $name;
}
public function setAddonIdent($ident) {
$this->addon_ident = $ident;
}
public function setAddonVersion($version) {
$this->addon_version = $version;
}
public function setAddonTempDir($path) {
$this->temp_dir = $path;
}
protected function copyInterfaceFiles() {
global $conf;
$install_dir = realpath($conf['rootpath'] . '/..');
if(is_dir($this->temp_dir . '/interface')) {
$ret = null;
$retval = 0;
$command = 'cp -rf ' . escapeshellarg($this->temp_dir . '/interface') . ' ' . escapeshellarg($install_dir . '/');
exec($command, $ret, $retval);
if($retval != 0) {
throw new AddonInstallerException('Command ' . $command . ' failed with code ' . $retval);
}
return true;
} else {
return false;
}
}
protected function copyServerFiles() {
global $conf;
$install_dir = realpath($conf['rootpath'] . '/..');
if(is_dir($this->temp_dir . '/server')) {
$ret = null;
$retval = 0;
$command = 'cp -rf ' . escapeshellarg($this->temp_dir . '/server'). ' ' . escapeshellarg($install_dir . '/');
exec($command, $ret, $retval);
if($retval != 0) {
throw new AddonInstallerException('Command ' . $command . ' failed with code ' . $retval);
}
return true;
} else {
return false;
}
}
protected function executeSqlStatements() {
global $app, $conf;
// create addon entry if not existing
$qry = 'INSERT IGNORE INTO `addons` (`addon_ident`, `addon_version`, `addon_name`, `db_version`) VALUES (?, ?, ?, ?)';
$app->db->query($qry, $this->addon_ident, $this->addon_version, $this->addon_name, 0);
$incremental = false;
$check = $app->db->queryOneRecord('SELECT `db_version` FROM `addons` WHERE `addon_ident` = ?', $this->addon_ident);
if($check) {
$incremental = 0;
if($check['db_version']) {
$incremental = $check['db_version'];
}
}
$mysql_command = 'mysql --default-character-set=' . escapeshellarg($conf['db_charset']) . ' --force -h ' . escapeshellarg($conf['db_host']) . ' -u ' . escapeshellarg($conf['db_user']) . ' -p' . escapeshellarg($conf['db_password']) . ' -P ' . escapeshellarg($conf['db_port']) . ' -D ' . escapeshellarg($conf['db_database']);
if($incremental === false) {
$sql_file = $this->temp_dir . '/install/sql/' . $this->addon_ident . '.sql';
if(is_file($sql_file)) {
$ret = null;
$retval = 0;
exec($mysql_command . ' < ' . escapeshellarg($sql_file), $ret, $retval);
if($retval != 0) {
/* TODO: log error! */
}
}
} else {
$new_db_version = $incremental;
while(true) {
$sql_file = $this->temp_dir . '/install/sql/incremental/upd_' . str_pad($new_db_version + 1, '0', 5, STR_PAD_LEFT) . '.sql';
if(!is_file($sql_file)) {
break;
} else {
$ret = null;
$retval = 0;
exec($mysql_command . ' < ' . escapeshellarg($sql_file), $ret, $retval);
if($retval != 0) {
/* TODO: log error! */
}
}
$new_db_version++;
}
$app->db->query('UPDATE `addons` SET `addon_version` = ?, `db_version` = ? WHERE `addon_ident` = ?', $this->addon_version, $new_db_version, $this->addon_ident);
}
return true;
}
public function onBeforeInstall() { }
public function onInstall() {
$this->copyInterfaceFiles();
$this->copyServerFiles();
$this->executeSqlStatements();
}
public function onAfterInstall() { }
public function onBeforeUpdate() { }
public function onUpdate() {
$this->copyInterfaceFiles();
$this->copyServerFiles();
$this->executeSqlStatements();
}
public function onAfterUpdate() { }
public function onRaisedInstallerEvent($event_name) {
}
}
class AddonInstallerException extends Exception {
public function __construct($message = "", $code = 0, $previous = null) {
parent::__construct($message, $code, $previous);
}
}
class AddonInstallerValidationException extends AddonInstallerException { }
\ No newline at end of file
......@@ -2044,7 +2044,32 @@ class system{
return true;
}
public function tempdir($parent_path = null, $prefix = 'tmp_', $mode = 0700) {
if(is_null($parent_path)) {
$parent_path = sys_get_temp_dir();
}
$parent_path = rtrim($parent_path, '/');
if(!is_dir($parent_path) || !is_writable($parent_path)) {
return false;
}
if(strpbrk($prefix, '\\/:*?"<>|') !== false) {
return false;
}
$path = false;
$tries = 0;
while($tries < 1000) {
$tries++;
$path = $parent_path . FS_DIV . uniqid($prefix, true);
if(mkdir($path, $mode)) {
break;
}
}
return $path;
}
}
?>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment