1<?php
2/**
3 * eGgroupWare setup - abstract baseclass for all setup commands, extending admin_cmd
4 *
5 * @link http://www.egroupware.org
6 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
7 * @package setup
8 * @copyright (c) 2007-16 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
9 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
10 * @version $Id$
11 */
12
13use EGroupware\Api;
14use EGroupware\Api\Egw;
15
16/**
17 * setup command: abstract baseclass for all setup commands, extending admin_cmd
18 */
19abstract class setup_cmd extends admin_cmd
20{
21	/**
22	 * Defaults set for empty options while running the command
23	 *
24	 * @var array
25	 */
26	public $set_defaults = array();
27
28	/**
29	 * Should be called by every command usually requiring header admin rights
30	 *
31	 * @throws Api\Exception\NoPermission(lang('Wrong credentials to access the header.inc.php file!'),2);
32	 */
33	protected function _check_header_access()
34	{
35		if (!$this->header_secret && $this->header_admin_user)	// no secret specified but header_admin_user/password
36		{
37			if (!$this->uid) $this->uid = true;
38			$this->set_header_secret($this->header_admin_user,$this->header_admin_password);
39		}
40		$secret = $this->_calc_header_secret($GLOBALS['egw_info']['server']['header_admin_user'],
41				$GLOBALS['egw_info']['server']['header_admin_password']);
42		if ($this->uid === true) unset($this->uid);
43
44		if ($this->header_secret != $secret)
45		{
46			//echo "_check_header_access: header_secret='$this->header_secret' != '$secret'=_calc_header_secret({$GLOBALS['egw_info']['server']['header_admin_user']},{$GLOBALS['egw_info']['server']['header_admin_password']})\n";
47			throw new Api\Exception\NoPermission(lang('Wrong credentials to access the header.inc.php file!'),5);
48		}
49
50	}
51
52	/**
53	 * Set the user and pw required for any operation on the header file
54	 *
55	 * @param string $user
56	 * @param string $pw password or md5 hash of it
57	 */
58	public function set_header_secret($user,$pw)
59	{
60		if ($this->uid || parent::save(false))	// we need to save first, to get the uid
61		{
62			$this->header_secret = $this->_calc_header_secret($user,$pw);
63		}
64		else
65		{
66			throw new Exception ('failed to set header_secret!');
67		}
68	}
69
70	/**
71	 * Calculate the header_secret used to access the header from this command
72	 *
73	 * It's an md5 over the uid, header-admin-user and -password.
74	 *
75	 * @param string $header_admin_user
76	 * @param string $header_admin_password
77	 * @return string
78	 */
79	private function _calc_header_secret($header_admin_user=null,$header_admin_password=null)
80	{
81		if (!self::is_md5($header_admin_password)) $header_admin_password = md5($header_admin_password);
82
83		$secret = md5($this->uid.$header_admin_user.$header_admin_password);
84		//echo "header_secret='$secret' = md5('$this->uid'.'$header_admin_user'.'$header_admin_password')\n";
85		return $secret;
86	}
87
88	/**
89	 * Saving the object to the database, reimplemented to not do it in setup context
90	 *
91	 * @param boolean $set_modifier =true set the current user as modifier or 0 (= run by the system)
92	 * @return boolean true on success, false otherwise
93	 */
94	function save($set_modifier=true)
95	{
96		if (isset($GLOBALS['egw']->db) && is_object($GLOBALS['egw']->db) && $GLOBALS['egw']->db->Database)
97		{
98			return parent::save($set_modifier);
99		}
100		return true;
101	}
102
103	/**
104	 * Reference to the setup object, after calling check_setup_auth method
105	 *
106	 * @var setup
107	 */
108	static protected $egw_setup;
109
110	static private $egw_accounts_backup;
111
112	/**
113	 * Create the setup enviroment (for running within setup or EGw)
114	 */
115	static protected function _setup_enviroment($domain=null)
116	{
117		if (!is_object($GLOBALS['egw_setup']))
118		{
119			require_once(EGW_INCLUDE_ROOT.'/setup/inc/class.setup.inc.php');
120			$GLOBALS['egw_setup'] = new setup(true,true);
121		}
122		self::$egw_setup = $GLOBALS['egw_setup'];
123		self::$egw_setup->ConfigDomain = $domain;
124
125		if (isset($GLOBALS['egw_info']['server']['header_admin_user']) && !isset($GLOBALS['egw_domain']) &&
126			is_object($GLOBALS['egw']) && $GLOBALS['egw'] instanceof Egw)
127		{
128			// we run inside EGw, not setup --> read egw_domain array from the header via the showheader cmd
129			$cmd = new setup_cmd_showheader(null);	// null = only header, no db stuff, no hashes
130			$header = $cmd->run();
131			$GLOBALS['egw_domain'] = $header['egw_domain'];
132
133			if (is_object($GLOBALS['egw']->accounts) && is_null(self::$egw_accounts_backup))
134			{
135				self::$egw_accounts_backup = $GLOBALS['egw']->accounts;
136				unset($GLOBALS['egw']->accounts);
137			}
138			if ($this->config) self::$egw_setup->setup_account_object($this->config);
139		}
140		if (is_object($GLOBALS['egw']->db) && $domain)
141		{
142			$GLOBALS['egw']->db->disconnect();
143			$GLOBALS['egw']->db = new Api\Db($GLOBALS['egw_domain'][$domain]);
144
145			// change caching to managed instance
146			Api\Cache::unset_instance_key();
147		}
148	}
149
150	/**
151	 * Restore EGw's db connection
152	 *
153	 */
154	static function restore_db()
155	{
156		if (is_object($GLOBALS['egw']->db))
157		{
158			$GLOBALS['egw']->db->disconnect();
159			$GLOBALS['egw']->db = new Api\Db($GLOBALS['egw_info']['server']);
160
161			// change caching back to own instance
162			Api\Cache::unset_instance_key();
163
164			if (!is_null(self::$egw_accounts_backup))
165			{
166				$GLOBALS['egw']->accounts = self::$egw_accounts_backup;
167				Api\Accounts::cache_invalidate();
168				unset(self::$egw_accounts_backup);
169			}
170		}
171	}
172
173	/**
174	 * Creates a setup like enviroment and checks for the header user/pw or config_user/pw if domain given
175	 *
176	 * @param string $user
177	 * @param string $pw
178	 * @param string $domain =null if given we also check agains config user/pw
179	 * @throws Api\Exception\NoPermission(lang('Access denied: wrong username or password for manage-header !!!'),21);
180	 * @throws Api\Exception\NoPermission(lang("Access denied: wrong username or password to configure the domain '%1(%2)' !!!",$domain,$GLOBALS['egw_domain'][$domain]['db_type']),40);
181	 */
182	static function check_setup_auth($user,$pw,$domain=null)
183	{
184		self::_setup_enviroment($domain);
185
186		// check the authentication if a header_admin_password is set, if not we dont have a header yet and no authentication
187		if ($GLOBALS['egw_info']['server']['header_admin_password'])	// if that's not given we dont have a header yet
188		{
189			if (!self::$egw_setup->check_auth($user,$pw,$GLOBALS['egw_info']['server']['header_admin_user'],
190					$GLOBALS['egw_info']['server']['header_admin_password']) &&
191				(is_null($domain) || !isset($GLOBALS['egw_domain'][$domain]) || // if valid domain given check it's config user/pw
192					!self::$egw_setup->check_auth($user,$pw,$GLOBALS['egw_domain'][$domain]['config_user'],
193						$GLOBALS['egw_domain'][$domain]['config_passwd'])))
194			{
195				if (is_null($domain))
196				{
197					throw new Api\Exception\NoPermission(lang('Access denied: wrong username or password for manage-header !!!'),21);
198				}
199				else
200				{
201					throw new Api\Exception\NoPermission(lang("Access denied: wrong username or password to configure the domain '%1(%2)' !!!",$domain,$GLOBALS['egw_domain'][$domain]['db_type']),40);
202				}
203			}
204		}
205	}
206
207	/**
208	 * Applications which are currently not installed (set after call to check_installed, for the last/only domain only)
209	 *
210	 * @var array
211	 */
212	static public $apps_to_install=array();
213	/**
214	 * Applications which are currently need update (set after call to check_installed, for the last/only domain only)
215	 *
216	 * @var array
217	 */
218	static public $apps_to_upgrade=array();
219
220	/**
221	 * Check if EGw is installed, which versions and if an update is needed
222	 *
223	 * Sets self::$apps_to_update and self::$apps_to_install for the last/only domain only!
224	 *
225	 * @param string $domain ='' domain to check, default '' = all
226	 * @param int/array $stop =0 stop checks before given exit-code(s), default 0 = all checks
227	 * @param boolean $verbose =false echo messages as they happen, instead returning them
228	 * @return array with translated messages
229	 */
230	static function check_installed($domain='',$stop=0,$verbose=false)
231	{
232		self::_setup_enviroment($domain);
233
234		global $setup_info;
235		static $header_checks=true;	// output the header checks only once
236
237		$messages = array();
238
239		if ($stop && !is_array($stop)) $stop = array($stop);
240
241		$versions =& $GLOBALS['egw_info']['server']['versions'];
242
243		if (!$versions['api'])
244		{
245			if (!include(EGW_INCLUDE_ROOT.'/api/setup/setup.inc.php'))
246			{
247				throw new Api\Exception\WrongUserinput(lang("EGroupware sources in '%1' are not complete, file '%2' missing !!!",realpath('..'),'api/setup/setup.inc.php'),99);	// should not happen ;-)
248			}
249			$versions['api'] = $setup_info['api']['version'];
250			unset($setup_info);
251		}
252		if ($header_checks)
253		{
254			$messages[] = self::_echo_message($verbose,lang('EGroupware API version %1 found.',$versions['api']));
255		}
256		$header_stage = self::$egw_setup->detection->check_header();
257		if ($stop && in_array($header_stage,$stop)) return true;
258
259		switch ($header_stage)
260		{
261			case 1: throw new Api\Exception\WrongUserinput(lang('EGroupware configuration file (header.inc.php) does NOT exist.')."\n".lang('Use --create-header to create the configuration file (--usage gives more options).'),1);
262
263//			case 2: throw new Api\Exception\WrongUserinput(lang('EGroupware configuration file (header.inc.php) version %1 exists%2',$versions['header'],'.')."\n".lang('No header admin password set! Use --edit-header <password>[,<user>] to set one (--usage gives more options).'),2);
264
265			case 3: throw new Api\Exception\WrongUserinput(lang('EGroupware configuration file (header.inc.php) version %1 exists%2',$versions['header'],'.')."\n".lang('No EGroupware domains / database instances exist! Use --edit-header --domain to add one (--usage gives more options).'),3);
266
267			case 4: throw new Api\Exception\WrongUserinput(lang('EGroupware configuration file (header.inc.php) version %1 exists%2',$versions['header'],'.')."\n".lang('It needs upgrading to version %1! Use --update-header <password>[,<user>] to do so (--usage gives more options).',$versions['current_header']),4);
268		}
269		if ($header_checks)
270		{
271			$messages[] = self::_echo_message($verbose,lang('EGroupware configuration file (header.inc.php) version %1 exists%2',
272				$versions['header'],' '.lang('and is up to date')));
273		}
274		unset($header_checks);	// no further output of the header checks
275
276		$domains = $GLOBALS['egw_domain'];
277		if ($domain)	// domain to check given
278		{
279			if (!isset($GLOBALS['egw_domain'][$domain])) throw new Api\Exception\WrongUserinput(lang("Domain '%1' does NOT exist !!!",$domain), 92);
280
281			$domains = array($domain => $GLOBALS['egw_domain'][$domain]);
282		}
283		foreach($domains as $domain => $data)
284		{
285			self::$egw_setup->ConfigDomain = $domain;	// set the domain the setup class operates on
286			if (count($GLOBALS['egw_domain']) > 1)
287			{
288				self::_echo_message($verbose);
289				$messages[] = self::_echo_message($verbose,lang('EGroupware domain/instance %1(%2):',$domain,$data['db_type']));
290			}
291			$setup_info = self::$egw_setup->detection->get_versions();
292			// check if there's already a db-connection and close if, otherwise the db-connection of the previous domain will be used
293			if (is_object(self::$egw_setup->db))
294			{
295				self::$egw_setup->db->disconnect();
296			}
297			self::$egw_setup->loaddb();
298
299			$db = $data['db_type'].'://'.$data['db_user'].':'.$data['db_pass'].'@'.$data['db_host'].'/'.$data['db_name'];
300
301			$db_stage =& $GLOBALS['egw_info']['setup']['stage']['db'];
302			if (($db_stage = self::$egw_setup->detection->check_db($setup_info)) != 1)
303			{
304				$setup_info = self::$egw_setup->detection->get_db_versions($setup_info);
305				$db_stage = self::$egw_setup->detection->check_db($setup_info);
306			}
307			if ($stop && in_array(10+$db_stage,$stop))
308			{
309				return $messages;
310			}
311			switch($db_stage)
312			{
313				case 1: throw new Api\Exception\WrongUserinput(lang('Your Database is not working!')." $db: ".self::$egw_setup->db->Error,11);
314
315				case 3: throw new Api\Exception\WrongUserinput(lang('Your database is working, but you dont have any applications installed')." ($db). ".lang("Use --install to install EGroupware."),13);
316
317				case 4: throw new Api\Exception\WrongUserinput(lang('EGroupware API needs a database (schema) update from version %1 to %2!',$setup_info['api']['currentver'],$versions['api']).' '.lang('Use --update to do so.'),14);
318
319				case 10:	// also check apps of updates
320					self::$apps_to_upgrade = self::$apps_to_install = array();
321					foreach($setup_info as $app => $data)
322					{
323						if ($data['currentver'] && $data['version'] && $data['version'] != 'deleted' && $data['version'] != $data['currentver'])
324						{
325							self::$apps_to_upgrade[] = $app;
326						}
327						if (!isset($data['enabled']) && isset($data['version']))	// jdots eg. is no app, but a template
328						{
329							self::$apps_to_install[] = $app;
330						}
331					}
332					// add autodeinstall apps
333					self::$apps_to_upgrade = array_unique(array_merge(self::$apps_to_upgrade, self::check_autodeinstall()));
334
335					if (self::$apps_to_install)
336					{
337						self::_echo_message($verbose);
338						$messages[] = self::_echo_message($verbose,lang('The following applications are NOT installed:').' '.implode(', ',self::$apps_to_install));
339					}
340					if (self::$apps_to_upgrade)
341					{
342						$db_stage = 4;
343						if ($stop && in_array(10+$db_stage,$stop)) return $messages;
344
345						throw new Api\Exception\WrongUserinput(lang('The following applications need to be upgraded:').' '.implode(', ',self::$apps_to_upgrade).'! '.lang('Use --update to do so.'),14);
346					}
347					break;
348			}
349			$messages[] = self::_echo_message($verbose,lang("database is version %1 and up to date.",$setup_info['api']['currentver']));
350
351			self::$egw_setup->detection->check_config();
352			if ($GLOBALS['egw_info']['setup']['config_errors'] && $stop && !in_array(15,$stop))
353			{
354				throw new Api\Exception\WrongUserinput(lang('You need to configure EGroupware:')."\n- ".@implode("\n- ",$GLOBALS['egw_info']['setup']['config_errors']),15);
355			}
356		}
357		return $messages;
358	}
359
360	/**
361	 * Check if there are apps which should be autoinstalled
362	 *
363	 * @return array with app-names
364	 */
365	static function check_autoinstall()
366	{
367		$ret = array_filter(self::$apps_to_install, function($app)
368		{
369			global $setup_info;
370			return $setup_info[$app]['autoinstall'];
371		});
372		//error_log(__METHOD__."() apps_to_install=".array2string(self::$apps_to_install).' returning '.array2string($ret));
373		return $ret;
374	}
375
376	/**
377	 * Check if app should be automatically deinstalled
378	 *
379	 * @return array with app-names to automatic deinstall
380	 */
381	static function check_autodeinstall()
382	{
383		global $setup_info;
384
385		$ret = array_values(array_filter(array_keys($setup_info), function($app)
386		{
387			global $setup_info;
388			if (empty($setup_info[$app]['autodeinstall']))
389			{
390				return false;
391			}
392			$autodeinstall = $setup_info[$app]['autodeinstall'];
393			if (!is_bool($autodeinstall))
394			{
395				try {
396					$autodeinstall = (bool)$GLOBALS['egw_setup']->db->query($autodeinstall, __LINE__, __FILE__)->fetchColumn();
397				}
398				catch (\Exception $e) {
399					_egw_log_exception($e);
400					$autodeinstall = false;
401				}
402			}
403			return $autodeinstall;
404		}));
405		//error_log(__METHOD__."() apps=".json_encode(array_keys($setup_info)).' returning '.json_encode($ret));
406		return $ret;
407	}
408
409	/**
410	 * Echo the given message, if $verbose
411	 *
412	 * @param boolean $verbose
413	 * @param string $msg
414	 * @return string $msg
415	 */
416	static function _echo_message($verbose,$msg='')
417	{
418		if ($verbose) echo $msg."\n";
419
420		return $msg;
421	}
422}
423