From c6d008734d72c0bf9bedf73f6881f159e0ed5cd8 Mon Sep 17 00:00:00 2001 From: Marius Burkard Date: Fri, 16 Nov 2018 15:35:23 +0100 Subject: [PATCH] - do not install/update addons during install/update of ISPconfig. This only leads to lots of trouble --- addons/empty.dir | 1 + install/install.php | 40 ---- .../classes/ispconfig_addon_installer.inc.php | 20 ++ install/lib/installer_base.lib.php | 17 +- .../sql/incremental/upd_dev_collection.sql | 9 + install/sql/ispconfig3.sql | 16 ++ install/update.php | 46 +---- interface/web/tools/resync.php | 13 ++ interface/web/tools/templates/resync.htm | 2 + server/lib/classes/addon_installer.inc.php | 178 ++++++++++++++++++ server/lib/classes/functions.inc.php | 6 + .../ispconfig_addon_installer_base.inc.php | 161 ++++++++++++++++ server/lib/classes/system.inc.php | 29 ++- 13 files changed, 444 insertions(+), 94 deletions(-) create mode 100644 addons/empty.dir create mode 100644 install/lib/classes/ispconfig_addon_installer.inc.php create mode 100644 server/lib/classes/addon_installer.inc.php create mode 100644 server/lib/classes/ispconfig_addon_installer_base.inc.php diff --git a/addons/empty.dir b/addons/empty.dir new file mode 100644 index 0000000000..95ba9ef37c --- /dev/null +++ b/addons/empty.dir @@ -0,0 +1 @@ +This empty directory is needed by ISPConfig. diff --git a/install/install.php b/install/install.php index e8e878ccd1..bb41927b33 100644 --- a/install/install.php +++ b/install/install.php @@ -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: ..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: ..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 //**************************************************************************************************** diff --git a/install/lib/classes/ispconfig_addon_installer.inc.php b/install/lib/classes/ispconfig_addon_installer.inc.php new file mode 100644 index 0000000000..78ef7e0b0c --- /dev/null +++ b/install/lib/classes/ispconfig_addon_installer.inc.php @@ -0,0 +1,20 @@ +addon_ident = preg_replace('/_addon_installer$/', '', get_called_class()); + } + + public function onRaisedInstallerEvent($event_name, $data) { + + } +} diff --git a/install/lib/installer_base.lib.php b/install/lib/installer_base.lib.php index fa7a7d3e5e..f3db824cd9 100644 --- a/install/lib/installer_base.lib.php +++ b/install/lib/installer_base.lib.php @@ -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)) { diff --git a/install/sql/incremental/upd_dev_collection.sql b/install/sql/incremental/upd_dev_collection.sql index 02a10ebfd2..dc5d4bb45c 100644 --- a/install/sql/incremental/upd_dev_collection.sql +++ b/install/sql/incremental/upd_dev_collection.sql @@ -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 ; diff --git a/install/sql/ispconfig3.sql b/install/sql/ispconfig3.sql index bed58a44f4..7462bbb74e 100644 --- a/install/sql/ispconfig3.sql +++ b/install/sql/ispconfig3.sql @@ -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` -- diff --git a/install/update.php b/install/update.php index 2fe3f6559f..3a51546f04 100644 --- a/install/update.php +++ b/install/update.php @@ -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: ..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: ..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 diff --git a/interface/web/tools/resync.php b/interface/web/tools/resync.php index b17cfb57db..b7cd965852 100644 --- a/interface/web/tools/resync.php +++ b/interface/web/tools/resync.php @@ -599,6 +599,19 @@ class page_action extends tform_actions { } $msg .= '
'; } + + $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 diff --git a/interface/web/tools/templates/resync.htm b/interface/web/tools/templates/resync.htm index 9d34254a34..d9d76a50e3 100644 --- a/interface/web/tools/templates/resync.htm +++ b/interface/web/tools/templates/resync.htm @@ -122,6 +122,8 @@
+ +{tmpl_hook name="end_form"}
diff --git a/server/lib/classes/addon_installer.inc.php b/server/lib/classes/addon_installer.inc.php new file mode 100644 index 0000000000..e7061f7836 --- /dev/null +++ b/server/lib/classes/addon_installer.inc.php @@ -0,0 +1,178 @@ +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; + } + +} diff --git a/server/lib/classes/functions.inc.php b/server/lib/classes/functions.inc.php index b20f3ab0eb..0470f4746d 100644 --- a/server/lib/classes/functions.inc.php +++ b/server/lib/classes/functions.inc.php @@ -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); diff --git a/server/lib/classes/ispconfig_addon_installer_base.inc.php b/server/lib/classes/ispconfig_addon_installer_base.inc.php new file mode 100644 index 0000000000..e971321ce5 --- /dev/null +++ b/server/lib/classes/ispconfig_addon_installer_base.inc.php @@ -0,0 +1,161 @@ +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 diff --git a/server/lib/classes/system.inc.php b/server/lib/classes/system.inc.php index adcc39f840..dc5a8dde8a 100644 --- a/server/lib/classes/system.inc.php +++ b/server/lib/classes/system.inc.php @@ -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; + } } - -?> -- GitLab