addon_installer.inc.php 7.92 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
<?php

/**
 * addon installer
 *
 * @author Marius Burkard
 */
class addon_installer {
	
	private function extractPackage($package_file) {
		global $app;
		
		$ret = null;
		$retval = 0;
		
Marius Burkard's avatar
Marius Burkard committed
16 17
		$app->log('Extracting addon package ' . $package_file, 0, false);
		
18 19 20
		$cmd = 'which unzip';
		$tmp = explode("\n", exec($cmd, $ret, $retval));
		if($retval != 0) {
Marius Burkard's avatar
Marius Burkard committed
21
			$app->log('The unzip command was not found on the server.', 2, false);
22 23 24 25 26
			throw new AddonInstallerException('unzip tool not found.');
		}
		$unzip = reset($tmp);
		unset($tmp);
		if(!$unzip) {
Marius Burkard's avatar
Marius Burkard committed
27
			$app->log('Unzip tool was not found.', 2, false);
28 29 30 31 32
			throw new AddonInstallerException('unzip tool not found.');
		}
		
		$temp_dir = $app->system->tempdir(sys_get_temp_dir(), 'addon_', 0700);
		if(!$temp_dir) {
Marius Burkard's avatar
Marius Burkard committed
33
			$app->log('Could not create the temp dir.', 2, false);
34 35 36 37 38 39 40 41
			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) {
Marius Burkard's avatar
Marius Burkard committed
42
			$app->log('Package extraction failed.', 2, false);
43 44 45
			throw new AddonInstallerException('Package extraction failed.');
		}
		
Marius Burkard's avatar
Marius Burkard committed
46 47
		$app->log('Extracted to ' . $temp_dir, 0, false);
		
48 49 50 51 52 53 54 55 56
		return $temp_dir;
	}

	/**
	 * @param string $path
	 * @return string
	 * @throws AddonInstallerValidationException
	 */
	private function validatePackage($path) {
Marius Burkard's avatar
Marius Burkard committed
57 58
		global $app;
		
Marius Burkard's avatar
Marius Burkard committed
59 60
		$app->log('Validating extracted addon at ' . $path, 0, false);
		
61
		if(!is_dir($path)) {
Marius Burkard's avatar
Marius Burkard committed
62
			$app->log('Invalid path.', 2, false);
63 64 65 66 67
			throw new AddonInstallerValidationException('Invalid path.');
		}
		
		$ini_file = $path . '/addon.ini';
		if(!is_file($ini_file)) {
Marius Burkard's avatar
Marius Burkard committed
68
			$app->log('Addon ini file missing.', 2, false);
69 70 71
			throw new AddonInstallerValidationException('Addon ini file missing.');
		}
		
Marius Burkard's avatar
Marius Burkard committed
72
		$app->log('Parsing ini ' . $ini_file, 0, false);
73 74
		$ini = parse_ini_file($ini_file, true);
		if(!$ini || !isset($ini['addon'])) {
Marius Burkard's avatar
Marius Burkard committed
75
			$app->log('Ini file could not be read.', 2, false);
76 77 78 79 80
			throw new AddonInstallerValidationException('Ini file is missing addon section.');
		}
		
		$addon = $ini['addon'];
		if(!isset($addon['ident']) || !isset($addon['name']) || !isset($addon['version'])) {
Marius Burkard's avatar
Marius Burkard committed
81
			$app->log('Addon data in ini file missing or invalid.', 2, false);
82 83 84 85 86
			throw new AddonInstallerValidationException('Ini file is missing addon ident/name/version.');
		}
		
		$class_file = $path . '/' . $addon['ident'] . '.addon.php';
		if(!is_file($class_file)) {
Marius Burkard's avatar
Marius Burkard committed
87
			$app->log('Base class file in addon not found', 2, false);
88 89 90 91
			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, '>')) {
Marius Burkard's avatar
Marius Burkard committed
92
			$app->log('ISPConfig version too low for this addon.', 2, false);
93 94
			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, '<')) {
Marius Burkard's avatar
Marius Burkard committed
95
			$app->log('ISPConfig version too high for this addon.', 2, false);
96 97 98
			throw new AddonInstallerValidationException('Addon allows at max ISPConfig version ' . $ini['ispconfig']['version.max'] . '.');
		}
		
Marius Burkard's avatar
Marius Burkard committed
99 100
		$app->log('Loaded addon installer ' . $class_file, 0, false);
		
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
		$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);
Marius Burkard's avatar
Marius Burkard committed
117 118 119 120 121
			if($addon && isset($addon['addon'])) {
				$addon = $addon['addon']; // ini section
			} else {
				$addon = false;
			}
122
			if(!$addon || !isset($addon['version']) || !isset($addon['ident']) || $addon['ident'] != $ident) {
Marius Burkard's avatar
Marius Burkard committed
123 124
				$app->log('Could not get version of installed addon.', 2, false);
				throw new AddonInstallerException('Installed app ' . $ident . ' found but it is invalid.');
125 126 127
			}
			
			$file_version = $addon['version'];
Marius Burkard's avatar
Marius Burkard committed
128
			$app->log('Installed version of addon ' . $ident . ' is ' . $file_version, 0, false);
129 130 131 132 133
		}
		
		$check = $app->db->queryOneRecord('SELECT `addon_version` FROM `addons` WHERE `addon_ident` = ?', $ident);
		if($check && $check['addon_version']) {
			$db_version = $check['addon_version'];
Marius Burkard's avatar
Marius Burkard committed
134
			$app->log('Installed version of addon ' . $ident . ' (in db) is ' . $db_version . '.', 0, false);
135 136 137 138 139
		}
		
		if(!$file_version && !$db_version) {
			return false;
		} elseif($file_version != $db_version) {
Marius Burkard's avatar
Marius Burkard committed
140
			$app->log('Version mismatch between ini file and database (' . $file_version . ' != ' . $db_version . ').', 0, false);
141 142 143 144 145 146 147 148 149 150 151 152 153 154
			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) {
155 156 157 158
		global $app;
		
		$app->load('ispconfig_addon_installer_base');
		
159
		if(!is_file($package_file)) {
Marius Burkard's avatar
Marius Burkard committed
160
			$app->log('Package file not found: ' . $package_file, 2, false);
161 162
			throw new AddonInstallerException('Package file not found.');
		} elseif(substr($package_file, -4) !== '.pkg') {
Marius Burkard's avatar
Marius Burkard committed
163
			$app->log('Invalid package file: ' . $package_file, 2, false);
164 165 166 167 168 169
			throw new AddonInstallerException('Invalid package file.');
		}
		
		$tmp_dir = $this->extractPackage($package_file);
		if(!$tmp_dir) {
			// extracting failed
Marius Burkard's avatar
Marius Burkard committed
170
			$app->log('Package extraction failed.', 2, false);
171 172 173 174 175 176 177
			throw new AddonInstallerException('Package extraction failed.');
		}
		
		$addon = $this->validatePackage($tmp_dir);
		if(!$addon) {
			throw new AddonInstallerException('Package validation failed.');
		}
Marius Burkard's avatar
Marius Burkard committed
178
		$app->log('Package validated.', 0, false);
179 180 181 182 183 184
		
		$is_update = false;
		$previous = $this->getInstalledAddonVersion($addon['ident']);
		if($previous !== false) {
			// this is an update
			if(version_compare($previous, $addon['version'], '>') && $force !== true) {
Marius Burkard's avatar
Marius Burkard committed
185
				$app->log('Installed version is newer than the one to install and --force not used.', 2, false);
186 187
				throw new AddonInstallerException('Installed version is newer than the one to install.');
			} elseif(version_compare($previous, $addon['version'], '=') && $force !== true) {
Marius Burkard's avatar
Marius Burkard committed
188
				$app->log('Installed version is the same as the one to install and --force not used.', 2, false);
189 190 191 192
				throw new AddonInstallerException('Installed version is the same as the one to install.');
			}
			$is_update = true;
		}
Marius Burkard's avatar
Marius Burkard committed
193 194 195
		
		$app->log('Including package class file ' . $addon['class_file'], 0, false);
		
196
		include $addon['class_file'];
Marius Burkard's avatar
Marius Burkard committed
197 198 199
		$class_name = $addon['class_name'];
		if(!class_exists($class_name)) {
			$app->log('Class name ' . $class_name . ' not found in class file ' . $addon['class_file'], 2, false);
200 201 202 203
			throw new AddonInstallerException('Could not find main class in addon file.');
		}
		
		/* @var $inst ispconfig_addon_installer_base */
Marius Burkard's avatar
Marius Burkard committed
204 205
		$app->log('Instanciating installer class ' . $class_name, 0, false);
		
206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221
		$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();
		}
		
Marius Burkard's avatar
Marius Burkard committed
222 223
		exec('rm -rf ' . escapeshellarg($tmp_dir));
		
Marius Burkard's avatar
Marius Burkard committed
224
		$app->log('Installation completed.', 0, false);
225 226 227 228
		return true;
	}
	
}