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