1<?php
2/**
3 * EGgroupware admin - admin command: change an account_id
4 *
5 * @link http://www.egroupware.org
6 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
7 * @package admin
8 * @copyright (c) 2007-19 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
9 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
10 */
11
12use EGroupware\Api;
13
14
15/**
16 * admin command: change an account_id
17 *
18 * @property boolean $group_renumbered =false true: group(s) have been renumbered by LDAP --> SQL migration,
19 *		do NOT change egw_accounts.account_id and egw_acl where acl_appname='phpgw_group'
20 */
21class admin_cmd_change_account_id extends admin_cmd
22{
23	/**
24	 * Constructor
25	 *
26	 * @param array $change array with old => new id pairs
27	 */
28	function __construct(array $change)
29	{
30		if (!isset($change['change']) && !($change['data'] && $change['id']))
31		{
32			$change = array(
33				'change' => $change,
34			);
35		}
36		admin_cmd::__construct($change);
37	}
38
39	/**
40	 * Query account columns from all apps
41	 *
42	 * Apps mark columns containing account-ids in "meta" attribute as (account|user|group)[-(abs|commasep|serialized)]
43	 *
44	 * @return array appname => array( table => array(column(s)))
45	 */
46	public static function get_account_colums()
47	{
48		// happens if one used "root_admin" and config-password
49		if (empty($GLOBALS['egw_info']['apps']))
50		{
51			$apps = new Api\Egw\Applications();
52			$apps->read_installed_apps();
53		}
54		$changes = $setup_info = array();
55		foreach(array_keys($GLOBALS['egw_info']['apps']) as $app)
56		{
57			if (!file_exists($path=EGW_SERVER_ROOT.'/'.$app.'/setup/setup.inc.php') || !include($path)) continue;
58
59			foreach((array)$setup_info[$app]['tables'] as $table)
60			{
61				if (!($definition = $GLOBALS['egw']->db->get_table_definitions($app, $table))) continue;
62
63				$cf = array();
64				foreach($definition['fd'] as $col => $data)
65				{
66					if (!empty($data['meta']))
67					{
68						foreach((array)$data['meta'] as $key => $val)
69						{
70							list($type, $subtype) = explode('-', $val.'-');
71							if (in_array($type, array('account', 'user', 'group')))
72							{
73								if (!is_numeric($key) || !empty($subtype))
74								{
75									$col = array($col);
76									if (!is_numeric($key)) $col[] = $key;
77									if (!empty($subtype)) $col['.type'] = $subtype;
78								}
79								$changes[$app][$table][] = $col;
80							}
81							if (in_array($type, array('cfname', 'cfvalue')))
82							{
83								$cf[$type] = $col;
84							}
85						}
86					}
87				}
88				// we have a custom field table and cfs containing accounts
89				if ($cf && !empty($cf['cfname']) && !empty($cf['cfvalue']) &&
90					($account_cfs = Api\Storage\Customfields::get_account_cfs($app == 'phpgwapi' ? 'addressbook' : $app)))
91				{
92					foreach($account_cfs as $type => $names)
93					{
94						unset($subtype);
95						list($type, $subtype) = explode('-', $type);
96						$col = array($cf['cfvalue']);
97						if (!empty($subtype)) $col['.type'] = $subtype;
98						$col[$cf['cfname']] = $names;
99						$changes[$app][$table][] = $col;
100					}
101				}
102			}
103			if (isset($changes[$app])) ksort($changes[$app]);
104		}
105		ksort($changes);
106		//print_r($changes);
107		return $changes;
108	}
109
110	/**
111	 * give or remove run rights from a given account and application
112	 *
113	 * @param boolean $check_only =false only run the checks (and throw the exceptions), but not the command itself
114	 * @return string success message
115	 * @throws Api\Exception\Permission\NoAdmin
116	 * @throws Api\Exception\WrongUserinput(lang("Unknown account: %1 !!!",$this->account),15);
117	 * @throws Api\Exception\WrongUserinput(lang("Application '%1' not found (maybe not installed or misspelled)!",$name),8);
118	 */
119	protected function exec($check_only=false)
120	{
121		$errors = array();
122		foreach($this->change as $from => $to)
123		{
124			if (!(int)$from || !(int)$to)
125			{
126				$errors[] = lang("Account-id's have to be integers!");
127			}
128			if (($from < 0) != ($to < 0))
129			{
130				$errors[] = lang("Can NOT change users into groups, same sign required!");
131			}
132			if (!$this->group_renumbered)
133			{
134				if (!($from_exists = $GLOBALS['egw']->accounts->exists($from)))
135				{
136					$errors[] = lang("Source account #%1 does NOT exist!", $from);
137				}
138				if ($from_exists !== ($from > 0 ? 1 : 2))
139				{
140					$errors[] = lang("Group #%1 must have negative sign!", $from);
141				}
142			}
143		}
144		if ($errors)
145		{
146			throw new Api\Exception\WrongUserinput(implode("\n", $errors), 16);
147		}
148		$columns2change = self::get_account_colums();
149		$total = 0;
150		foreach($columns2change as $app => $data)
151		{
152			if (!isset($GLOBALS['egw_info']['apps'][$app])) continue;	// $app is not installed
153
154			$db = clone($GLOBALS['egw']->db);
155			$db->set_app($app);
156			if ($check_only) $db->log_updates = $db->readonly = true;
157
158			foreach($data as $table => $columns)
159			{
160				$db->column_definitions = $db->get_table_definitions($app,$table);
161				$db->column_definitions = $db->column_definitions['fd'];
162				if (!$columns)
163				{
164					$this->value .= "$app: $table no columns with account-id's\n";
165					continue;	// noting to do for this table
166				}
167				if (!is_array($columns)) $columns = array($columns);
168
169				foreach($columns as $column)
170				{
171					$type = $where = null;
172					if (is_array($column))
173					{
174						$type = $column['.type'];
175						unset($column['.type']);
176						$where = $column;
177						$column = array_shift($where);
178					}
179					if ($this->group_renumbered && $table == 'egw_accounts' && $column == 'account_id')
180					{
181						continue;
182					}
183					if ($this->group_renumbered && $table == 'egw_acl')
184					{
185						$where[] = "acl_appname != 'phpgw_group'";
186					}
187					$total += ($changed = self::_update_account_id($this->change,$db,$table,$column,$where,$type));
188					if (!$check_only && $changed) $this->value .=  "$app:\t$table.$column $changed id's changed\n";
189				}
190			}
191		}
192		if (!$check_only)
193		{
194			foreach($GLOBALS['egw_info']['apps'] as $app => $data)
195			{
196				$total += ($changed = Api\Framework\Favorites::change_account_ids($app, $this->change));
197				if ($changed) $this->value .=  "$app:\t$changed id's in favorites or index-state changed\n";
198			}
199
200			// call hooks, in case apps need additional changes
201			$args = $this->change;
202			$args['location'] = 'change_account_ids';
203			foreach(Api\Hooks::process($args, array(), true) as $app => $changed)
204			{
205				$total += $changed;
206				if ($changed) $this->value .=  "$app:\t$changed id's changed by application hook\n";
207			}
208		}
209		echo $this->value;
210		if ($total) Api\Cache::flush(Api\Cache::INSTANCE);
211
212		return lang("Total of %1 id's changed.",$total)."\n";
213	}
214
215	/**
216	 * Update DB with changed account ids
217	 *
218	 * @param array $ids2change from-id => to-id pairs
219	 * @param Api\Db $db
220	 * @param string $table
221	 * @param string $column
222	 * @param array $where
223	 * @param string $type
224	 * @return int number of changed ids
225	 */
226	private static function _update_account_id(array $ids2change,Api\Db $db,$table,$column,array $where=null,$type=null)
227	{
228		$update_sql = '';
229		foreach($ids2change as $from => $to)
230		{
231			$update_sql .= "WHEN ".$db->quote($from,$db->column_definitions[$column]['type'])." THEN ".$db->quote($to,$db->column_definitions[$column]['type'])." ";
232		}
233		$update_sql .= 'END';
234
235		// check if we have a timestamp column with default current_timestamp
236		// in that case we need to set the timestamp to it's current value,
237		// to not update it to the current time and thereby loosing its value
238		$extra_set = '';
239		$extra_set_array = array();
240		if (($table_def = $db->get_table_definitions(true, $table)))
241		{
242			foreach($table_def['fd'] as $col => $data)
243			{
244				if ($data['type'] === 'timestamp' && $data['default'] === 'current_timestamp')
245				{
246					$extra_set .= ($extra_set ? ',' : '').$col.'='.$col;
247				}
248			}
249			if (!empty($extra_set))
250			{
251				$extra_set_array[] = $extra_set;
252				$extra_set .= ',';
253			}
254		}
255
256
257		switch($type)
258		{
259			case 'commasep':
260			case 'serialized':
261				if (!$where) $where = array();
262				$select = $where;
263				$select[] = "$column IS NOT NULL";
264				$select[] = "$column != ''";
265				$change = array();
266				foreach($db->select($table,'DISTINCT '.$column,$select,__LINE__,__FILE__) as $row)
267				{
268					$ids = $type != 'serialized' ? explode(',',$old_ids=$row[$column]) : json_php_unserialize($old_ids=$row[$column]);
269					foreach($ids as $key => $id)
270					{
271						if (isset($ids2change[$id])) $ids[$key] = $ids2change[$id];
272					}
273					$ids2 = $type != 'serialized' ? implode(',',$ids) : serialize($ids);
274					if ($ids2 != $old_ids)
275					{
276						$change[$old_ids] = $ids2;
277					}
278				}
279				$changed = 0;
280				foreach($change as $from => $to)
281				{
282					$db->update($table, array($column=>$to)+$extra_set_array,
283						$where+array($column=>$from), __LINE__, __FILE__);
284					$changed += $db->affected_rows();
285				}
286				break;
287
288			case 'abs':
289				if (!$where) $where = array();
290				$where[$column] = array();
291				foreach($ids2change as $from => $to)
292				{
293					$where[$column][] = abs($from);
294				}
295				$db->update($table, $extra_set.$column.'= CASE '.$column.' '.preg_replace('/-([0-9]+)/', '\1', $update_sql),
296					$where, __LINE__, __FILE__);
297				$changed = $db->affected_rows();
298				break;
299
300			case 'prefs':	// prefs groups are shifted down by 2 as -1 and -2 are for default and forced prefs
301				if (!$where) $where = array();
302				$where[$column] = array();
303				$update_sql = '';
304				foreach($ids2change as $from => $to)
305				{
306					if ($from < 0) $from -= 2;
307					if ($to < 0) $to -= 2;
308					$where[$column][] = $from;
309					$update_sql .= 'WHEN '.$db->quote($from,$db->column_definitions[$column]['type']).' THEN '.$db->quote($to,$db->column_definitions[$column]['type']).' ';
310				}
311				$db->update($table, $extra_set.$column.'= CASE '.$column.' '.$update_sql.'END',
312					$where, __LINE__, __FILE__);
313				$changed = $db->affected_rows();
314				break;
315
316			default:
317				if (!$where) $where = array();
318				$where[$column] = array_keys($ids2change);
319				$db->update($table, $extra_set.$column.'= CASE '.$column.' '.$update_sql,
320					$where, __LINE__, __FILE__);
321				$changed = $db->affected_rows();
322				break;
323		}
324		return $changed;
325	}
326
327	/**
328	 * Return a title / string representation for a given command, eg. to display it
329	 *
330	 * @return string
331	 */
332	function __tostring()
333	{
334		$change = array();
335		foreach($this->change as $from => $to)
336		{
337			$change[] = $from.'->'.$to;
338		}
339		return lang('Change account_id').': '.implode(', ',$change);
340	}
341}
342