Issues (4868)

admin/inc/class.admin_ui.inc.php (5 issues)

1
<?php
2
/**
3
 * EGroupware: Admin app UI
4
 *
5
 * @link http://www.egroupware.org
6
 * @author Ralf Becker <[email protected]>
7
 * @package admin
8
 * @copyright (c) 2013-16 by Ralf Becker <[email protected]>
9
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
10
 * @version $Id$
11
 */
12
13
use EGroupware\Api;
14
use EGroupware\Api\Egw;
15
use EGroupware\Api\Etemplate;
16
use EGroupware\Api\Etemplate\Widget\Tree;
17
use EGroupware\Api\Link;
18
19
/**
20
 * UI for admin
21
 */
22
class admin_ui
23
{
24
	/**
25
	 * Methods callable via menuaction
26
	 * @var array
27
	 */
28
	public $public_functions = array(
29
		'index' => true,
30
	);
31
32
	/**
33
	 * Reference to global accounts object
34
	 *
35
	 * @var Api\Accounts
36
	 */
37
	private static $accounts;
38
39
	/**
40
	 * New index page
41
	 *
42
	 * @param array $content
43
	 */
44
	public function index(array $content=null)
45
	{
46
		if (admin_statistics::check(false))
47
		{
48
			$_GET['load'] = 'admin.admin_statistics.submit';
49
			$_GET['ajax'] = 'false';
50
			$_GET['required'] = 'true';
51
		}
52
		$tpl = new Etemplate('admin.index');
53
54
		if (!is_array($content)) $content = array();
55
		$content['nm'] = array(
56
			'get_rows' => 'admin_ui::get_users',
57
			'no_cat' => true,
58
			'no_filter2' => true,
59
			'filter_no_lang' => true,
60
			'lettersearch' => true,
61
			'order' => 'account_lid',
62
			'sort' => 'ASC',
63
			'row_id' => 'account_id',
64
			'default_cols' => '!account_id,created,account_lastlogin,account_lastloginfrom,account_lastpwd_change',
65
			'actions' => self::user_actions(),
66
			'placeholder_actions' => array('add')
67
		);
68
		$content['groups'] = array(
69
			'get_rows'   => 'admin_ui::get_groups',
70
			'no_cat'     => true,
71
			'no_filter'  => true,
72
			'no_filter2' => true,
73
			'row_id'     => 'account_id',
74
			'actions'    => self::group_actions(),
75
			'placeholder_actions' => array('add')
76
		);
77
78
		//$content['msg'] = 'Hi Ralf ;-)';
79
		$sel_options['tree'] = $this->tree_data();
0 ignored issues
show
Comprehensibility Best Practice introduced by
$sel_options was never initialized. Although not strictly required by PHP, it is generally a good practice to add $sel_options = array(); before regardless.
Loading history...
80
		$sel_options['filter'] = array('' => lang('All groups'));
81
		foreach(self::$accounts->search(array(
82
			'type' => 'groups',
83
			'start' => false,
84
			'order' => 'account_lid',
85
			'sort' => 'ASC',
86
		)) as $data)
87
		{
88
			$sel_options['filter'][$data['account_id']] = empty($data['account_description']) ? $data['account_lid'] : array(
89
				'label' => $data['account_lid'],
90
				'title' => $data['account_description'],
91
			);
92
		}
93
		$sel_options['account_primary_group'] = $sel_options['filter'];
94
		unset($sel_options['account_primary_group']['']);
95
96
		$tpl->setElementAttribute('tree', 'actions', self::tree_actions());
97
98
		// switching between iframe and nm/accounts-list depending on load parameter
99
		// important for first time load eg. from an other application calling it's site configuration
100
		$tpl->setElementAttribute('iframe', 'disabled', empty($_GET['load']));
101
		$content['iframe'] = 'about:blank';	// we show accounts-list be default now
102
		if (!empty($_GET['load']))
103
		{
104
			$vars = $_GET;
105
			$vars['menuaction'] = $vars['load'];
106
			unset($vars['load']);
107
			$content[$vars['ajax'] ? 'ajax_target':'iframe'] = Egw::link('/index.php', $vars);
108
		}
109
110
		$tpl->exec('admin.admin_ui.index', $content, $sel_options);
111
	}
112
113
	/**
114
	 * Actions on tree / groups
115
	 *
116
	 * @return array
117
	 */
118
	public static function tree_actions()
119
	{
120
		$actions = static::group_actions();
121
122
		foreach($actions as $action_id =>  &$action)
123
		{
124
			if (!isset($action['enableId']) && !in_array($action_id, array('add')))
125
			{
126
				$action['enableId'] = '^/groups/-\\d+';
127
			}
128
		}
129
130
		return $actions;
131
	}
132
133
	/**
134
	 * Actions on users
135
	 *
136
	 * @return array
137
	 */
138
	public static function user_actions()
139
	{
140
		static $actions = null;
141
142
		if (!isset($actions))
143
		{
144
			$actions = array(
145
				'edit' => array(
146
					'caption' => 'Open',
147
					'default' => true,
148
					'allowOnMultiple' => false,
149
					'onExecute' => 'javaScript:app.admin.account',
150
					'group' => $group=0,
151
				),
152
				'add' => array(
153
					'caption' => 'Add user',
154
					'onExecute' => 'javaScript:app.admin.account',
155
					'group' => $group,
156
				),
157
				'copy' => array(
158
					'caption' => 'Copy',
159
					'url' => 'menuaction=addressbook.addressbook_ui.edit&makecp=1&contact_id=$id',
160
					'onExecute' => 'javaScript:app.admin.account',
161
					'allowOnMultiple' => false,
162
					'icon' => 'copy',
163
				),
164
			);
165
			// generate urls for add/edit accounts via addressbook
166
			$edit = Link::get_registry('addressbook', 'edit');
167
			$edit['account_id'] = '$id';
168
			foreach($edit as $name => $val)
0 ignored issues
show
The expression $edit of type boolean|string is not traversable.
Loading history...
169
			{
170
				$actions['edit']['url'] .= ($actions['edit']['url'] ? '&' : '').$name.'='.$val;
171
			}
172
			unset($edit['account_id']);
173
			$edit['owner'] = 0;
174
			foreach($edit as $name => $val)
0 ignored issues
show
The expression $edit of type boolean|string is not traversable.
Loading history...
175
			{
176
				$actions['add']['url'] .= ($actions['edit']['url'] ? '&' : '').$name.'='.$val;
177
			}
178
			++$group;
179
			// supporting both old way using $GLOBALS['menuData'] and new just returning data in hook
180
			$apps = array_unique(array_merge(array('admin'), Api\Hooks::implemented('edit_user')));
181
			foreach($apps as $app)
182
			{
183
				$GLOBALS['menuData'] = $data = array();
0 ignored issues
show
The assignment to $data is dead and can be removed.
Loading history...
184
				$data = Api\Hooks::single('edit_user', $app);
185
				if (!is_array($data)) $data = $GLOBALS['menuData'];
186
				foreach($data as $item)
187
				{
188
					// allow hook to return "real" actions, but still support legacy: description, url, extradata, options
189
					if (empty($item['caption']))
190
					{
191
						$item['caption'] = $item['description'];
192
						unset($item['description']);
193
					}
194
					if (isset($item['url']) && isset($item['extradata']))
195
					{
196
						$item['url'] = $item['extradata'].'&account_id=$id';
197
						$item['id'] = substr($item['extradata'], 11);
198
						unset($item['extradata']);
199
						$matches = null;
200
						if ($item['options'] && preg_match('/(egw_openWindowCentered2?|window.open)\([^)]+,(\d+),(\d+).*(title="([^"]+)")?/', $item['options'], $matches))
201
						{
202
							$item['popup'] = $matches[2].'x'.$matches[3];
203
							if (isset($matches[5])) $item['tooltip'] = $matches[5];
204
							unset($item['options']);
205
						}
206
					}
207
					if (empty($item['icon'])) $item['icon'] = $app.'/navbar';
208
					if (empty($item['group'])) $item['group'] = $group;
209
					if (empty($item['onExecute'])) $item['onExecute'] = $item['popup'] ?
210
						'javaScript:nm_action' : 'javaScript:app.admin.iframe_location';
211
					if (!isset($item['allowOnMultiple'])) $item['allowOnMultiple'] = false;
212
213
					$actions[$item['id']] = $item;
214
				}
215
			}
216
			$actions['delete'] = array(
217
				'caption' => 'Delete',
218
				'group' => ++$group,
219
				'popup' => '615x600',
220
				'url' => 'menuaction=admin.admin_account.delete&account_id=$id',
221
				'allowOnMultiple' => false,
222
			);
223
		}
224
		//error_log(__METHOD__."() actions=".array2string($actions));
225
		return $actions;
226
	}
227
228
	/**
229
	 * Actions on groups
230
	 *
231
	 * @return array
232
	 */
233
	public static function group_actions()
234
	{
235
		$user_actions = self::user_actions();
236
		$actions = array(
237
			'view' => array(
238
				'onExecute' => 'javaScript:app.admin.group',
239
				'caption' => 'Show members',
240
				'default' => true,
241
				'group' => $group=1,
242
				'allowOnMultiple' => false
243
			),
244
			'add' => array(
245
				'group' => $group,
246
			)+$user_actions['add'],
247
			'acl' => array(
248
				'onExecute' => 'javaScript:app.admin.group',
249
				'caption' => 'Access control',
250
				'url' => 'menuaction=admin.admin_acl.index&account_id=$id',
251
				'popup' => '900x450',
252
				'icon' => 'lock',
253
				'group' => 2,
254
				'allowOnMultiple' => false
255
			),
256
		);
257
		if (!$GLOBALS['egw']->acl->check('account_access',64,'admin'))	// no rights to set ACL-rights
258
		{
259
			$actions['deny'] = array(
260
				'caption'   => 'Deny access',
261
				'url'       => 'menuaction=admin.admin_denyaccess.list_apps&account_id=$id',
262
				'onExecute' => 'javaScript:app.admin.group',
263
				'icon'      => 'cancel',
264
				'group'     => 2,
265
				'allowOnMultiple' => false
266
			);
267
		}
268
269
		$group = 5;	// allow to place actions in different groups by hook, this is the default
270
271
		// supporting both old way using $GLOBALS['menuData'] and new just returning data in hook
272
		$apps = array_unique(array_merge(array('admin'), Api\Hooks::implemented('edit_group')));
273
		foreach($apps as $app)
274
		{
275
			$GLOBALS['menuData'] = $data = array();
0 ignored issues
show
The assignment to $data is dead and can be removed.
Loading history...
276
			$data = Api\Hooks::single('edit_group', $app);
277
			if (!is_array($data)) $data = $GLOBALS['menuData'];
278
279
			foreach($data as $item)
280
			{
281
				// allow hook to return "real" actions, but still support legacy: description, url, extradata, options
282
				if (empty($item['caption']))
283
				{
284
					$item['caption'] = $item['description'];
285
					unset($item['description']);
286
				}
287
				if (isset($item['url']) && isset($item['extradata']))
288
				{
289
					$item['url'] = $item['extradata'].'&account_id=$id';
290
					$item['id'] = substr($item['extradata'], 11);
291
					unset($item['extradata']);
292
					$matches = null;
293
					if ($item['options'] && preg_match('/(egw_openWindowCentered2?|window.open)\([^)]+,(\d+),(\d+).*(title="([^"]+)")?/', $item['options'], $matches))
294
					{
295
						$item['popup'] = $matches[2].'x'.$matches[3];
296
						$item['onExecute'] = 'javaScript:nm_action';
297
						if (isset($matches[5])) $item['tooltip'] = $matches[5];
298
						unset($item['options']);
299
					}
300
				}
301
				if (empty($item['icon'])) $item['icon'] = $app.'/navbar';
302
				if (empty($item['group'])) $item['group'] = $group;
303
				if (empty($item['onExecute'])) $item['onExecute'] = 'javaScript:app.admin.group';
304
				if (!isset($item['allowOnMultiple'])) $item['allowOnMultiple'] = false;
305
306
				$actions[$item['id']] = $item;
307
			}
308
		}
309
		return $actions;
310
	}
311
312
	/**
313
	 * Callback for nextmatch to fetch users
314
	 *
315
	 * @param array $query
316
	 * @param array &$rows=null
317
	 * @return int total number of rows available
318
	 */
319
	public static function get_users(array $query, array &$rows=null)
320
	{
321
		$params = array(
322
			'type' => (int)$query['filter'] ? (int)$query['filter'] : 'accounts',
323
			'start' => $query['start'],
324
			'offset' => $query['num_rows'],
325
			'order' => $query['order'],
326
			'sort' => $query['sort'],
327
			'active' => !empty($query['active']) ? $query['active'] : false,
328
		);
329
		if ($query['searchletter'])
330
		{
331
			$params['query'] = $query['searchletter'];
332
			$params['query_type'] = 'start';
333
		}
334
		elseif($query['search'])
335
		{
336
			$params['query'] = $query['search'];
337
			$params['query_type'] = 'all';
338
		}
339
340
		$rows = array_values(self::$accounts->search($params));
341
		//error_log(__METHOD__."() accounts->search(".array2string($params).") total=".self::$accounts->total);
342
343
		foreach($rows as &$row)
344
		{
345
			$row['status'] = self::$accounts->is_expired($row) ?
346
				lang('Expired').' '.Api\DateTime::to($row['account_expires'], true) :
347
					(!self::$accounts->is_active($row) ? lang('Disabled') :
348
						($row['account_expires'] != -1 ? lang('Expires').' '.Api\DateTime::to($row['account_expires'], true) :
349
							lang('Enabled')));
350
351
			if (!self::$accounts->is_active($row)) $row['status_class'] = 'adminAccountInactive';
352
		}
353
354
		return self::$accounts->total;
355
	}
356
357
	/**
358
	 * Callback for the nextmatch to get groups
359
	 */
360
	public static function get_groups(&$query, &$rows)
361
	{
362
		$groups = $GLOBALS['egw']->accounts->search(array(
363
				'type'  => 'groups',
364
				'query' => $query['search'],
365
				'order' => $query['order'],
366
				'sort'  => $query['sort'],
367
				'start' => (int)$query['start'],
368
				'offset' => (int)$query['num_rows']
369
			));
370
371
		$apps = array();
372
		foreach($GLOBALS['egw_info']['apps'] as $app => $data)
373
		{
374
			if (!$data['enabled'] || !$data['status'] || $data['status'] == 3)
375
			{
376
				continue;	// do NOT show disabled apps, or our API (status = 3)
377
			}
378
379
			$apps[] = $app;
380
		}
381
382
		$rows = array();
383
		foreach($groups as &$group)
384
		{
385
			$run_rights = $GLOBALS['egw']->acl->get_user_applications($group['account_id'], false, false);
386
			foreach($apps as $app)
387
			{
388
				if((boolean)$run_rights[$app])
389
				{
390
					$group['apps'][] = $app;
391
				}
392
			}
393
394
			$group['members'] = $GLOBALS['egw']->accounts->members($group['account_id'],true);
395
			$rows[] = $group;
396
		}
397
398
		return $GLOBALS['egw']->accounts->total;
399
	}
400
401
	/**
402
	 * Autoload tree from $_GET['id'] on
403
	 */
404
	public static function ajax_tree()
405
	{
406
		Etemplate\Widget\Tree::send_quote_json(self::tree_data(!empty($_GET['id']) ? $_GET['id'] : '/'));
407
	}
408
409
	/**
410
	 * Get data for navigation tree
411
	 *
412
	 * Example:
413
	 * array(
414
	 *	'id' => 0, 'item' => array(
415
	 *		array('id' => '/INBOX', 'text' => 'INBOX', 'tooltip' => 'Your inbox', 'open' => 1, 'im1' => 'kfm_home.png', 'im2' => 'kfm_home.png', 'child' => '1', 'item' => array(
416
	 *			array('id' => '/INBOX/sub', 'text' => 'sub', 'im0' => 'folderClosed.gif'),
417
	 *			array('id' => '/INBOX/sub2', 'text' => 'sub2', 'im0' => 'folderClosed.gif'),
418
	 *		)),
419
	 *		array('id' => '/user', 'text' => 'user', 'child' => '1', 'item' => array(
420
	 *			array('id' => '/user/birgit', 'text' => 'birgit', 'im0' => 'folderClosed.gif'),
421
	 *		)),
422
	 * ));
423
	 *
424
	 * @param string $root ='/'
425
	 * @return array
426
	 */
427
	public static function tree_data($root = '/')
428
	{
429
		$tree = array(Tree::ID => $root === '/' ? 0 : $root, Tree::CHILDREN => array(), 'child' => 1);
430
431
		if ($root == '/')
432
		{
433
			$hook_data = self::call_hook();
434
			foreach($hook_data as $app => $app_data)
435
			{
436
				if(!is_array($app_data))
437
				{
438
					// Application has no data
439
					continue;
440
				}
441
				foreach($app_data as $text => $data)
442
				{
443
					if (!is_array($data))
444
					{
445
						$data = array(
446
							'link' => $data,
447
						);
448
					}
449
					if (empty($data[Tree::LABEL])) $data[Tree::LABEL] = $text;
450
					if (empty($data[Tree::ID]))
451
					{
452
						$data['id'] = $root.($app == 'admin' ? 'admin' : 'apps/'.$app).'/';
453
						$matches = null;
454
						if (preg_match_all('/(menuaction|load)=([^&]+)/', $data['link'], $matches))
455
						{
456
							$data[Tree::ID] .= $matches[2][(int)array_search('load', $matches[1])];
457
						}
458
					}
459
					if (!empty($data['icon']))
460
					{
461
						$icon = Etemplate\Widget\Tree::imagePath($data['icon']);
462
						if ($data['child'] || $data[Tree::CHILDREN])
463
						{
464
							$data[Tree::IMAGE_FOLDER_OPEN] = $data[Tree::IMAGE_FOLDER_CLOSED] = $icon;
465
						}
466
						else
467
						{
468
							$data[Tree::IMAGE_LEAF] = $icon;
469
						}
470
					}
471
					unset($data['icon']);
472
					$parent =& $tree[Tree::CHILDREN];
473
					$parts = explode('/', $data[Tree::ID]);
474
					if ($data[Tree::ID][0] == '/') array_shift($parts);	// remove root
475
					array_pop($parts);
476
					$path = '';
477
					foreach($parts as $part)
478
					{
479
						$path .= ($path == '/' ? '' : '/').$part;
480
						if (!isset($parent[$path]))
481
						{
482
							$icon = Etemplate\Widget\Tree::imagePath($part == 'apps' ? Api\Image::find('api', 'home') :
483
								(($i=Api\Image::find($part, 'navbar')) ? $i : Api\Image::find('api', 'nonav')));
484
							$parent[$path] = array(
485
								Tree::ID => $path,
486
								Tree::LABEL => $part == 'apps' ? lang('Applications') : lang($part),
487
								//'im0' => 'folderOpen.gif',
488
								Tree::IMAGE_FOLDER_OPEN => $icon,
489
								Tree::IMAGE_FOLDER_CLOSED => $icon,
490
								Tree::CHILDREN => array(),
491
								'child' => 1,
492
							);
493
							if ($path == '/admin') $parent[$path]['open'] = true;
494
						}
495
						$parent =& $parent[$path][Tree::CHILDREN];
496
					}
497
					$data[Tree::LABEL] = lang($data[Tree::LABEL]);
498
					if (!empty($data['tooltip'])) $data['tooltip'] = lang($data['tooltip']);
499
					// make sure keys are unique, as we overwrite tree entries otherwise
500
					if (isset($parent[$data[Tree::ID]])) $data[Tree::ID] .= md5($data['link']);
501
					$parent[$data[Tree::ID]] = self::fix_userdata($data);
502
				}
503
			}
504
		}
505
		elseif ($root == '/groups')
506
		{
507
			foreach($GLOBALS['egw']->accounts->search(array(
508
				'type' => 'groups',
509
				'order' => 'account_lid',
510
				'sort' => 'ASC',
511
			)) as $group)
512
			{
513
				$tree[Tree::CHILDREN][] = self::fix_userdata(array(
514
					Tree::LABEL => $group['account_lid'],
515
					Tree::TOOLTIP => $group['account_description'],
516
					Tree::ID => $root.'/'.$group['account_id'],
517
				));
518
			}
519
		}
520
		self::strip_item_keys($tree[Tree::CHILDREN]);
521
		//_debug_array($tree); exit;
522
		return $tree;
523
	}
524
525
	/**
526
	 * Fix userdata as understood by tree
527
	 *
528
	 * @param array $data
529
	 * @return array
530
	 */
531
	private static function fix_userdata(array $data)
532
	{
533
		if(!$data[Tree::LABEL])
534
		{
535
			$data[Tree::LABEL] = $data['text'];
536
		}
537
		// store link as userdata, maybe we should store everything not directly understood by tree this way ...
538
		foreach(array_diff_key($data, array_flip(array(
539
			Tree::ID,Tree::LABEL,Tree::TOOLTIP,'im0','im1','im2','item','child','select','open','call',
540
		))) as $name => $content)
541
		{
542
			$data['userdata'][] = array(
543
				'name' => $name,
544
				'content' => $content,
545
			);
546
			unset($data[$name]);
547
		}
548
		return $data;
549
	}
550
551
	/**
552
	 * Attribute 'item' has to be an array
553
	 *
554
	 * @param array $items
555
	 */
556
	private static function strip_item_keys(array &$items)
557
	{
558
		$items = array_values($items);
559
		foreach($items as &$item)
560
		{
561
			if (is_array($item) && isset($item['item']))
562
			{
563
				self::strip_item_keys($item['item']);
564
			}
565
		}
566
	}
567
568
	public static $hook_data = array();
569
	/**
570
	 * Return data from regular admin hook calling display_section() instead of returning it
571
	 *
572
	 * @return array appname => array of label => link/data pairs
573
	 */
574
	protected static function call_hook()
575
	{
576
		self::$hook_data = array();
577
		function display_section($appname,$file,$file2=False)
578
		{
579
			admin_ui::$hook_data[$appname] = $file2 ? $file2 : $file;
580
			//error_log(__METHOD__."(".array2string(func_get_args()).")");
581
		}
582
		self::$hook_data = array_merge(Api\Hooks::process('admin', array('admin')), self::$hook_data);
583
584
		// sort apps alphabetic by their title / Api\Translation of app-name
585
		uksort(self::$hook_data, function($a, $b)
586
		{
587
			return strcasecmp(lang($a), lang($b));
588
		});
589
		// make sure admin is first
590
		self::$hook_data = array_merge(array('admin' => self::$hook_data['admin']), self::$hook_data);
591
592
		return self::$hook_data;
593
	}
594
595
	/**
596
	 * Init static variables
597
	 */
598
	public static function init_static()
599
	{
600
		self::$accounts = $GLOBALS['egw']->accounts;
601
	}
602
}
603
admin_ui::init_static();
604