Issues (4868)

admin/inc/class.admin_acl.inc.php (7 issues)

1
<?php
2
/**
3
 * EGroupware: Admin ACL
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\Framework;
15
use EGroupware\Api\Acl;
16
use EGroupware\Api\Etemplate;
17
18
/**
19
 * UI for admin ACL
20
 *
21
 * Will also be extended by preferences_acl for user ACL
22
 */
23
class admin_acl
24
{
25
	/**
26
	 * Methods callable via menuaction
27
	 * @var array
28
	 */
29
	public $public_functions = array(
30
		'index' => true,
31
	);
32
33
	/**
34
	 * Reference to global Acl class (instanciated for current user)
35
	 *
36
	 * @var Acl
37
	 */
38
	protected $acl;
39
40
	/**
41
	 * Constructor
42
	 */
43
	public function __construct()
44
	{
45
		$this->acl = $GLOBALS['egw']->acl;
46
	}
47
48
	/**
49
	 * Save run rights and refresh opener
50
	 *
51
	 * @param array $content
52
	 */
53
	protected function save_run_rights(array $content)
54
	{
55
		$old_apps = array_keys($this->acl->get_user_applications($content['acl_account'], false, false));
56
		// add new apps
57
		$added_apps = array_diff($content['apps'], $old_apps);
58
		foreach($added_apps as $app)
59
		{
60
			$this->acl->add_repository($app, 'run', $content['acl_account'], 1);
61
		}
62
		// remove no longer checked apps
63
		$removed_apps = array_diff($old_apps, $content['apps']);
64
		$deleted_ids = array();
65
		foreach($removed_apps as $app)
66
		{
67
			$this->acl->delete_repository($app, 'run', $content['acl_account']);
68
			$deleted_ids[] = $app.':'.$content['acl_account'].':run';
69
		}
70
		//error_log(__METHOD__."() apps=".array2string($content['apps']).", old_apps=".array2string($old_apps).", added_apps=".array2string($added_apps).", removed_apps=".array2string($removed_apps));
71
72
		if (!$added_apps && !$removed_apps)
0 ignored issues
show
Bug Best Practice introduced by
The expression $added_apps of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
Bug Best Practice introduced by
The expression $removed_apps of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
73
		{
74
			// nothing changed --> nothing to do/notify
75
		}
76
		elseif (!$old_apps)
0 ignored issues
show
Bug Best Practice introduced by
The expression $old_apps of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
77
		{
78
			Framework::refresh_opener(lang('ACL added.'), 'admin', null, 'add');
79
		}
80
		elseif (!$added_apps)
0 ignored issues
show
Bug Best Practice introduced by
The expression $added_apps of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
81
		{
82
			Framework::refresh_opener(lang('ACL deleted.'), 'admin', $deleted_ids, 'delete');
83
		}
84
		else
85
		{
86
			Framework::refresh_opener(lang('ACL updated.'), 'admin', null, 'edit');
87
		}
88
	}
89
90
	/**
91
	 * Save rights and refresh opener
92
	 *
93
	 * @param array $content
94
	 */
95
	protected function save_rights(array $content)
96
	{
97
		// assamble rights again
98
		$rights = (int)$content['preserve_rights'];
99
		foreach($content['acl'] as $right)
100
		{
101
			$rights |= $right;
102
		}
103
		$id = !empty($content['id']) ? $content['id'] :
104
		$content['acl_appname'].':'.$content['acl_account'].':'.$content['acl_location'];
105
		//error_log(__METHOD__."() id=$id, acl=".array2string($content['acl'])." --> rights=$rights");
106
107
		if ($this->acl->get_specific_rights_for_account($content['acl_account'], $content['acl_location'], $content['acl_appname']) == $rights)
108
		{
109
			// nothing changed --> nothing to do
110
		}
111
		elseif (!$rights)	// all rights removed --> delete it
112
		{
113
			$this->acl->delete_repository($content['acl_appname'], $content['acl_location'], $content['acl_account']);
114
			Framework::refresh_opener(lang('ACL deleted.'), 'admin', $id, 'delete');
115
		}
116
		else
117
		{
118
			$this->acl->add_repository($content['acl_appname'], $content['acl_location'], $content['acl_account'], $rights);
119
			if ($content['id'])
120
			{
121
				Framework::refresh_opener(lang('ACL updated.'), 'admin', $id, 'edit');
122
			}
123
			else
124
			{
125
				Framework::refresh_opener(lang('ACL added.'), 'admin', $id, 'add');
126
			}
127
		}
128
	}
129
130
	/**
131
	 * Callback for nextmatch to fetch Acl
132
	 *
133
	 * @param array $query
134
	 * @param array &$rows=null
135
	 * @return int total number of rows available
136
	 */
137
	public static function get_rows(array $query, array &$rows=null)
138
	{
139
		$so_sql = new Api\Storage\Base('phpgwapi', Acl::TABLE, null, '', true);
140
141
		// client queries destinct rows by their row-id
142
		if (isset($query['col_filter']['id']))
143
		{
144
			$query['col_filter'][] = $GLOBALS['egw']->db->concat('acl_appname',"':'",'acl_account',"':'",'acl_location').
145
				' IN ('.implode(',', array_map(array($GLOBALS['egw']->db, 'quote'), (array)$query['col_filter']['id'])).')';
146
			unset($query['col_filter']['id']);
147
		}
148
		else
149
		{
150
			$memberships = $GLOBALS['egw']->accounts->memberships($query['account_id'], true);
151
			$memberships[] = $query['account_id'];
152
153
			Api\Cache::setSession(__CLASS__, 'state', array(
154
				'account_id' => $query['account_id'],
155
				'filter' => $query['filter'],
156
				'acl_appname' => $query['filter2'],
157
			));
158
159
			if ($GLOBALS['egw_info']['user']['preferences']['admin']['acl_filter'] != $query['filter'])
160
			{
161
				$GLOBALS['egw']->preferences->add('admin', 'acl_filter', $query['filter']);
162
				$GLOBALS['egw']->preferences->save_repository(false,'user',false);
163
			}
164
			switch($query['filter'])
165
			{
166
				case 'run':
167
					$query['col_filter']['acl_location'] = 'run';
168
					$query['col_filter']['acl_account'] = $memberships;
169
					break;
170
				default:
171
				case 'other':
172
					//$query['col_filter'][] = "acl_location!='run'";
173
					// remove everything not an account-id in location, like category based acl
174
					if (substr($GLOBALS['egw']->db->Type, 0, 5) == 'mysql')
175
					{
176
						$query['col_filter'][] = "acl_location REGEXP '^-?[0-9]+$'";
177
					}
178
					else
179
					{
180
						$query['col_filter'][] = "acl_location SIMILAR TO '-?[0-9]+'";
181
					}
182
					// get apps not using group-acl (eg. Addressbook) or using it only partialy (eg. InfoLog)
183
					$not_enum_group_acls = Api\Hooks::process('not_enum_group_acls', array(), true);
184
					//error_log(__METHOD__."(filter=$query[filter]) not_enum_group_acl=".array2string($not_enum_group_acls));
185
					if ($not_enum_group_acls)
186
					{
187
						$sql = '(CASE acl_appname';
188
						foreach($not_enum_group_acls as $app => $groups)
189
						{
190
							if ($groups === true)
191
							{
192
								$check = $query['account_id'];
193
							}
194
							else
195
							{
196
								$check = array_diff($memberships, $groups);
197
								//error_log(__METHOD__."() app=$app, array_diff(memberships=".array2string($memberships).", groups=".array2string($groups).")=".array2string($check));
198
								if (!$check) continue;	// would give sql error otherwise!
0 ignored issues
show
Bug Best Practice introduced by
The expression $check of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
199
							}
200
							$sql .= ' WHEN '.$GLOBALS['egw']->db->quote($app).' THEN '.$GLOBALS['egw']->db->expression(Acl::TABLE, array(
201
								'acl_account' => $check,
202
							));
203
						}
204
						$sql .= ' ELSE ';
205
					}
206
					$sql .= $GLOBALS['egw']->db->expression(Acl::TABLE, array(
207
						'acl_account' => $memberships,
208
					));
209
					if ($not_enum_group_acls) $sql .= ' END)';
210
					$query['col_filter'][] = $sql;
211
					break;
212
213
				case 'own':
214
					$query['col_filter']['acl_location'] = $memberships;
215
					break;
216
			}
217
			// do NOT list group-memberships and other non-ACL stuff here
218
			$query['col_filter']['acl_appname'] = $query['filter2'];
219
			if (empty($query['col_filter']['acl_appname']) && $query['filter'] != 'run')
220
			{
221
				//$query['col_filter'][] = "NOT acl_appname IN ('phpgw_group','sqlfs')";
222
				$query['col_filter']['acl_appname'] = array_keys($query['acl_rights']);
223
			}
224
		}
225
		$readonlys = array();
226
		$total = $so_sql->get_rows($query, $rows, $readonlys);
227
228
		foreach($rows as &$row)
229
		{
230
			// generate a row-id
231
			$row['id'] = $row['acl_appname'].':'.$row['acl_account'].':'.$row['acl_location'];
232
233
			if ($row['acl_location'] == 'run')
234
			{
235
				$row['acl1'] = lang('run');
236
			}
237
			else
238
			{
239
				if ($app !== $row['acl_appname']) Api\Translation::add_app($row['app_name']);
240
				foreach($query['acl_rights'][$row['acl_appname']] as $val => $label)
241
				{
242
					if ($row['acl_rights'] & $val)
243
					{
244
						$row['acl'.$val] = lang($label);
245
					}
246
				}
247
			}
248
			if (!self::check_access($row['acl_account'], $row['acl_location'], false))	// false: do NOT throw an exception!
249
			{
250
				$row['class'] = 'rowNoEdit';
251
			}
252
			//error_log(__METHOD__."() $n: ".array2string($row));
253
		}
254
		//error_log(__METHOD__."(".array2string($query).") returning ".$total);
255
256
		// Get supporting or all apps for filter2 depending on filter
257
		if($query['filter'] == 'run')
258
		{
259
			$rows['sel_options']['filter2'] = array(
260
				'' => lang('All applications'),
261
			)+Etemplate\Widget\Select::app_options('enabled');
262
		}
263
		else
264
		{
265
			$rows['sel_options']['filter2'] = array(
266
				array('value' => '', 'label' => lang('All applications'))
267
			);
268
			$apps = Api\Hooks::process(array(
269
				'location' => 'acl_rights',
270
				'owner' => $query['account_id'],
271
			), array(), true);
272
			foreach(array_keys($apps) as $appname)
273
			{
274
				$rows['sel_options']['filter2'][] = array(
275
					'value' => $appname,
276
					'label' => lang($appname)
277
				);
278
			}
279
			usort($rows['sel_options']['filter2'], function($a,$b) {
280
				return strcasecmp($a['label'], $b['label']);
281
			});
282
		}
283
284
		return $total;
285
	}
286
287
	/**
288
	 * Check if current user has access to ACL setting of a given location
289
	 *
290
	 * @param int $account_id numeric account-id
291
	 * @param int|string $location =null numeric account-id or "run"
292
	 * @param boolean $throw =true if true, throw an exception if no access, instead of just returning false
293
	 * @return boolean true if access is granted, false if notification_bo
294
	 * @throws Api\Exception\NoPermission
295
	 */
296
	public static function check_access($account_id, $location=null, $throw=true)
297
	{
298
		static $admin_access=null;
299
		static $own_access=null;
300
		if (is_null($admin_access))
301
		{
302
			$admin_access = isset($GLOBALS['egw_info']['user']['apps']['admin']) &&
303
				!$GLOBALS['egw']->acl->check('account_access', 64, 'admin');	// ! because this denies access!
304
			$own_access = $admin_access || isset($GLOBALS['egw_info']['user']['apps']['preferences']);
305
		}
306
		if (!(int)$account_id || !((int)$account_id == (int)$GLOBALS['egw_info']['user']['account_id'] && $location !== 'run' ?
307
				$own_access : $admin_access))
308
		{
309
			if ($throw) throw new Api\Exception\NoPermission(lang('Permission denied!!!'));
310
			return false;
311
		}
312
		return true;
313
	}
314
315
	/**
316
	 * Get the list of applications allowed for the given user
317
	 *
318
	 * The list of applications is added to the json response
319
	 *
320
	 * @param int $account_id
321
	 */
322
	public static function ajax_get_app_list($account_id)
323
	{
324
		$list = array();
325
		if(self::check_access((int)$account_id,'run',false))
326
		{
327
			$list = array_keys($GLOBALS['egw']->acl->get_user_applications((int)$account_id,false,false));
328
		}
329
		Api\Json\Response::get()->data($list);
330
	}
331
332
	/**
333
	 * Change (add, modify, delete) an ACL entry
334
	 *
335
	 * Checks access and throws an exception, if a change is attempted without proper access
336
	 *
337
	 * @param string|array $ids "$app:$account:$location" string used as row-id in list
338
	 * @param int $rights null to delete, or new rights
339
	 * @param array $values Additional values from UI
340
	 * @param string $etemplate_exec_id to check against CSRF
341
	 * @throws Api\Exception\NoPermission
342
	 */
343
	public static function ajax_change_acl($ids, $rights, $values, $etemplate_exec_id)
344
	{
345
		Api\Etemplate\Request::csrfCheck($etemplate_exec_id, __METHOD__, func_get_args());
346
347
		try {
348
			foreach((array)$ids as $id)
349
			{
350
				list($app, $account_id, $location) = explode(':', $id, 3);
351
352
				self::check_access($account_id, $location);	// throws exception, if no rights
353
354
				$acl = $GLOBALS['egw']->acl;
355
356
				if($location == 'run')
357
				{
358
					$right_list = array(1 => 'run');
359
				}
360
				else
361
				{
362
					$right_list = Api\Hooks::single(array(
363
						'location' => 'acl_rights',
364
						'owner' => $location,
365
					), $app);
366
				}
367
				$current = (int)$acl->get_specific_rights_for_account($account_id,$location,$app);
368
				foreach(array_keys((array)$right_list) as $right)
369
				{
370
					$have_it = !!($current & $right);
371
					$set_it = !!($rights & $right);
372
					if($have_it == $set_it) continue;
373
					$data = array(
374
						'allow' => $set_it,
375
						'account' => $account_id,
376
						'app' => $app,
377
						'location' => $location,
378
						'rights' => (int)$right
379
						// This is the documentation from policy app
380
					)+(array)$values['admin_cmd'];
381
					if($location == 'run')
382
					{
383
						$cmd = new admin_cmd_account_app($set_it,$account_id, $app, (array)$values['admin_cmd']);
384
					}
385
					else
386
					{
387
						$cmd = new admin_cmd_acl($data);
388
					}
389
					$cmd->run();
390
				}
391
			}
392
			if (!(int)$rights)
393
			{
394
				if (count($ids) > 1)
395
				{
396
					$msg = lang('%1 ACL entries deleted.', count($ids));
0 ignored issues
show
The call to lang() has too many arguments starting with count($ids). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

396
					$msg = /** @scrutinizer ignore-call */ lang('%1 ACL entries deleted.', count($ids));

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
397
				}
398
				else
399
				{
400
					$msg = lang('ACL entry deleted.');
401
				}
402
			}
403
			else
404
			{
405
				$msg = lang('ACL updated');
406
			}
407
			Api\Json\Response::get()->data(array(
408
				'msg' => $msg,
409
				'ids' => $ids,
410
				'type' => !(int)$rights ? 'delete' : 'add',
411
			));
412
		}
413
		catch (Exception $e) {
414
			Api\Json\Response::get()->call('egw.message', $e->getMessage(), 'error');
415
		}
416
	}
417
418
	/**
419
	 * New index page
420
	 *
421
	 * @param array $_content =null
422
	 */
423
	public function index(array $_content=null)
424
	{
425
		unset($_content);	// not used, required by function signature
426
427
		$tpl = new Etemplate('admin.acl');
428
429
		$content = array();
430
		$account_id = isset($_GET['account_id']) && (int)$_GET['account_id'] ?
431
			(int)$_GET['account_id'] : $GLOBALS['egw_info']['user']['account_id'];
432
		$content['nm'] = array(
433
			'get_rows' => 'admin_acl::get_rows',
434
			'no_cat' => true,
435
			'filter' => !empty($_GET['acl_filter']) ? $_GET['acl_filter'] :
436
				($GLOBALS['egw_info']['flags']['currentapp'] != 'admin' ? 'other' :
437
					$GLOBALS['egw_info']['user']['preferences']['admin']['acl_filter']),
438
			'filter2' => !empty($_GET['acl_app']) ? $_GET['acl_app'] : '',
439
			'lettersearch' => false,
440
			'order' => 'acl_appname',
441
			'sort' => 'ASC',
442
			'row_id' => 'id',
443
			'account_id' => $account_id,
444
			'actions' => self::get_actions(),
445
			'acl_rights' => Api\Hooks::process(array(
446
				'location' => 'acl_rights',
447
				'owner' => $account_id,
448
			), array(), true),
449
		);
450
		$user = Api\Accounts::username($content['nm']['account_id']);
451
		$sel_options = array(
452
			'filter' => array(
453
				'other' => lang('Access to %1 data by others', $user),
0 ignored issues
show
The call to lang() has too many arguments starting with $user. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

453
				'other' => /** @scrutinizer ignore-call */ lang('Access to %1 data by others', $user),

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
454
				'own'   => lang('%1 access to other data', $user),
455
				'run'   => lang('%1 run rights for applications', $user),
456
			)
457
		);
458
459
		// Set this so if loaded via preferences, js is still properly
460
		// loaded into global app.admin
461
		$GLOBALS['egw_info']['flags']['currentapp'] = 'admin';
462
463
		$tpl->exec('admin.admin_acl.index', $content, $sel_options, array(), array(), 2);
464
	}
465
466
	/**
467
	 * Get actions for ACL
468
	 *
469
	 * @return array
470
	 */
471
	static function get_actions()
472
	{
473
		return array(
474
			'edit' => array(
475
				'caption' => 'Edit',
476
				'default' => true,
477
				'allowOnMultiple' => false,
478
				'disableClass' => 'rowNoEdit',
479
				'onExecute' => 'javaScript:app.admin.acl',
480
			),
481
			'add' => array(
482
				'caption' => 'Add',
483
				'disableClass' => 'rowNoEdit',
484
				'onExecute' => 'javaScript:app.admin.acl',
485
			),
486
			'delete' => array(
487
				'caption' => 'Delete',
488
				'disableClass' => 'rowNoEdit',
489
				'onExecute' => 'javaScript:app.admin.acl',
490
			),
491
		);
492
	}
493
}
494