admin_cmd::search()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 9
dl 0
loc 5
rs 10
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
/**
3
 * EGroupware admin - admin command base class
4
 *
5
 * @link http://www.egroupware.org
6
 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
7
 * @package admin
8
 * @copyright (c) 2007-18 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
9
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
10
 */
11
12
use EGroupware\Api;
13
use EGroupware\Api\Acl;
14
15
/**
16
 * Admin comand base class
17
 *
18
 * Admin commands should be used to implement and log (!) all actions admins carry
19
 * out using the administrative rights (regular users cant do).
20
 *
21
 * They are stored in DB table egw_admin_queue which builds a persitent log
22
 * of administrative actions cared out on an EGroupware installation.
23
 * Commands can be marked deleted (canceled for scheduled commands),
24
 * but they are never deleted for the table to implement a persistent log!
25
 *
26
 * All administrative actions are encapsulated in classes derived from this
27
 * abstract base class implementing an exec method to carry out the command.
28
 *
29
 * @property-read int $created Creation timestamp
30
 * @property-read int $creator Creator user-id
31
 * @property-read string $creator_email rfc822 address ("Name <[email protected]>") of creator
32
 * @property-read int $modified Modification timestamp
33
 * @property-read int|NULL $scheduled timestamp if command is not run immediatly,
34
 *	but scheduled to run automatic by the system at a later point in time
35
 * @property-read int $modifier Modifier user-id
36
 * @property-read string $modifier_email rfc822 address ("Name <[email protected]>") of modifier
37
 * @property int|NULL $requested User who requested the change (not current user!)
38
 * @property string|NULL $requested_email rfc822 address ("Name <[email protected]>") of requested
39
 * @property string|NULL $comment comment, eg. reasoning why change was requested
40
 * @property-read int|NULL $errno Numerical error-code or NULL on success
41
 * @property-read string|NULL $error Error message or NULL on success
42
 * @property array|string|NULL $result Result message indicating what happened, or NULL on failure
43
 * @property-read int $id $id of command/row in egw_admin_queue table
44
 * @property-read string $uid uuid of command (necessary if command is send to a remote system to execute)
45
 * @property int|NULL $remote_id id of remote system, if command is not meant to run on local system
46
 *  foreign key into egw_admin_remote (table of remote systems administrated by this one)
47
 * @property-read int $account account_id of user affected by this cmd or NULL
48
 * @property-read string $app app-name affected by this cmd or NULL
49
 * @property-read string $parent parent cmd (with rrule) of single periodic execution
50
 * @property-read string $rrule rrule for periodic execution
51
 * @property int $rrule_start optional start timestamp for rrule, default $created time
52
 * @property string async_job_id optional name of async job for periodic-run, default "admin-cmd-$id"
53
 * @property array set optional New values set by the command
54
 * @property array old optional Previous values before the command was run
55
 */
56
abstract class admin_cmd
57
{
58
	const deleted    = 0;
0 ignored issues
show
Coding Style introduced by
This class constant is not uppercase (expected DELETED).
Loading history...
59
	const scheduled  = 1;
0 ignored issues
show
Coding Style introduced by
This class constant is not uppercase (expected SCHEDULED).
Loading history...
60
	const successful = 2;
0 ignored issues
show
Coding Style introduced by
This class constant is not uppercase (expected SUCCESSFUL).
Loading history...
61
	const failed     = 3;
0 ignored issues
show
Coding Style introduced by
This class constant is not uppercase (expected FAILED).
Loading history...
62
	const pending    = 4;
0 ignored issues
show
Coding Style introduced by
This class constant is not uppercase (expected PENDING).
Loading history...
63
	const queued     = 5;	// command waits to be fetched from remote
0 ignored issues
show
Coding Style introduced by
This class constant is not uppercase (expected QUEUED).
Loading history...
64
65
	/**
66
	 * Status which stil need passwords available
67
	 *
68
	 * @var array
69
	 */
70
	static $require_pw_stati = array(self::scheduled,self::pending,self::queued);
71
72
	/**
73
	 * The status of the command, one of either scheduled, successful, failed or deleted
74
	 *
75
	 * @var int
76
	 */
77
	protected $status = self::successful;
78
79
	static $stati = array(
80
		admin_cmd::scheduled  => 'scheduled',
81
		admin_cmd::successful => 'successful',
82
		admin_cmd::failed     => 'failed',
83
		admin_cmd::deleted    => 'deleted',
84
		admin_cmd::pending    => 'pending',
85
		admin_cmd::queued     => 'queued',
86
	);
87
88
	protected $created;
89
	protected $creator;
90
	protected $creator_email;
91
	private $scheduled;
92
	private $modified;
93
	private $modifier;
94
	private $modifier_email;
95
	protected $error;
96
	protected $errno;
97
	public $requested;
98
	public $requested_email;
99
	public $comment;
100
	private $id;
101
	protected $uid;
102
	private $type = __CLASS__;
103
	public $remote_id;
104
	protected $account;
105
	protected $app;
106
	protected $rrule;
107
	protected $parent;
108
109
	/**
110
	 * Display name of command, default ucfirst(str_replace(['_cmd_', '_'], ' ', __CLASS__))
111
	 */
112
	const NAME = null;
113
114
	/**
115
	 * Stores the data of the derived classes
116
	 *
117
	 * @var array
118
	 */
119
	private $data = array();
120
121
	/**
122
	 * Instance of the Api\Accounts class, after calling instanciate_accounts!
123
	 *
124
	 * @var Api\Accounts
125
	 */
126
	static protected $accounts;
127
128
	/**
129
	 * Instance of the Acl class, after calling instanciate_acl!
130
	 *
131
	 * @var Acl
132
	 */
133
	static protected $acl;
134
135
	/**
136
	 * Instance of Api\Storage\Base for egw_admin_queue
137
	 *
138
	 * @var Api\Storage\Base
139
	 */
140
	static private $sql;
141
142
	/**
143
	 * Instance of Api\Storage\Base for egw_admin_remote
144
	 *
145
	 * @var Api\Storage\Base
146
	 */
147
	static private $remote;
148
149
	/**
150
	 * Executes the command
151
	 *
152
	 * @param boolean $check_only =false only run the checks (and throw the exceptions), but not the command itself
153
	 * @return string success message
154
	 * @throws Exception()
155
	 */
156
	protected abstract function exec($check_only=false);
157
158
	/**
159
	 * Return a title / string representation for a given command, eg. to display it
160
	 *
161
	 * @return string
162
	 */
163
	function __tostring()
164
	{
165
		return $this->type;
166
	}
167
168
	/**
169
	 * Generate human readable name of object
170
	 *
171
	 * @return string
172
	 */
173
	public static function name()
174
	{
175
		if (self::NAME) return self::NAME;
176
177
		return ucfirst(str_replace(['_cmd_', '_', '\\'], ' ', get_called_class()));
178
	}
179
180
	/**
181
	 * Constructor
182
	 *
183
	 * @param array $data class vars as array
184
	 */
185
	function __construct(array $data)
186
	{
187
		$this->created = time();
0 ignored issues
show
Bug introduced by
The property created is declared read-only in admin_cmd.
Loading history...
188
		$this->creator = $GLOBALS['egw_info']['user']['account_id'];
0 ignored issues
show
Bug introduced by
The property creator is declared read-only in admin_cmd.
Loading history...
189
		$this->creator_email = admin_cmd::user_email();
0 ignored issues
show
Bug introduced by
The property creator_email is declared read-only in admin_cmd.
Loading history...
190
191
		$this->type = get_class($this);
192
193
		foreach($data as $name => $value)
194
		{
195
			$this->$name = $name == 'data' && !is_array($value) ? json_php_unserialize($value) : $value;
196
		}
197
		//_debug_array($this); exit;
198
	}
199
200
	/**
201
	 * runs the command either immediatly ($time=null) or shedules it for the given time
202
	 *
203
	 * The command will be written to the database queue, incl. its scheduled start time or execution status
204
	 *
205
	 * @param int $time =null timestamp to run the command or null to run it immediatly
206
	 * @param boolean $set_modifier =null should the current user be set as modifier, default true
207
	 * @param booelan $skip_checks =false do not yet run the checks for a scheduled command
0 ignored issues
show
Bug introduced by
The type booelan was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
208
	 * @param boolean $dry_run =false only run checks, NOT command itself
209
	 * @return mixed return value of the command
210
	 * @throws Exceptions on error
211
	 */
212
	function run($time=null,$set_modifier=true,$skip_checks=false,$dry_run=false)
213
	{
214
		if (!is_null($time))
215
		{
216
			$this->scheduled = $time;
0 ignored issues
show
Bug introduced by
The property scheduled is declared read-only in admin_cmd.
Loading history...
217
			$this->status = admin_cmd::scheduled;
218
			$ret = lang('Command scheduled to run at %1',date('Y-m-d H:i',$time));
0 ignored issues
show
Unused Code introduced by
The call to lang() has too many arguments starting with date('Y-m-d H:i', $time). ( Ignorable by Annotation )

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

218
			$ret = /** @scrutinizer ignore-call */ lang('Command scheduled to run at %1',date('Y-m-d H:i',$time));

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...
219
			// running the checks of the arguments for local commands, if not explicitly requested to not run them
220
			if (!$this->remote_id && !$skip_checks)
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->remote_id of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
221
			{
222
				try {
223
					$this->exec(true);
224
				}
225
				catch (Exception $e) {
226
					_egw_log_exception($e);
227
					$this->error = $e->getMessage();
0 ignored issues
show
Bug introduced by
The property error is declared read-only in admin_cmd.
Loading history...
228
					$ret = $this->errno = $e->getCode();
0 ignored issues
show
Bug introduced by
The property errno is declared read-only in admin_cmd.
Loading history...
229
					$this->status = admin_cmd::failed;
230
					$dont_save = true;
231
				}
232
			}
233
		}
234
		else
235
		{
236
			try {
237
				if (!$this->remote_id)
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->remote_id of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
238
				{
239
					$ret = $this->exec($dry_run);
240
				}
241
				else
242
				{
243
					$ret = $this->remote_exec($dry_run);
0 ignored issues
show
Unused Code introduced by
The call to admin_cmd::remote_exec() has too many arguments starting with $dry_run. ( Ignorable by Annotation )

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

243
					/** @scrutinizer ignore-call */ 
244
     $ret = $this->remote_exec($dry_run);

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...
244
				}
245
				if (is_null($this->status)) $this->status = admin_cmd::successful;
0 ignored issues
show
introduced by
The condition is_null($this->status) is always false.
Loading history...
246
			}
247
			catch (Exception $e) {
248
				_egw_log_exception($e);
249
				$this->error = $e->getMessage();
250
				$ret = $this->errno = $e->getCode();
251
				$this->status = admin_cmd::failed;
252
			}
253
		}
254
		$this->result = $ret;
255
		if (!$dont_save && !$dry_run && !$this->save($set_modifier))
256
		{
257
			throw new Api\Db\Exception(lang('Error saving the command!'));
258
		}
259
		if ($e instanceof Exception)
260
		{
261
			throw $e;
262
		}
263
		return $ret;
264
	}
265
266
	/**
267
	 * Runs a command on a remote install
268
	 *
269
	 * This is a very basic remote procedure call to an other egw instance.
270
	 * The payload / command data is send as POST request to the remote installs admin/remote.php script.
271
	 * The remote domain (eGW instance) and the secret authenticating the request are send as GET parameters.
272
	 *
273
	 * To authenticate with the installation we use a secret, which is a md5 hash build from the uid
274
	 * of the command (to not allow to send new commands with an earsdroped secret) and the md5 hash
275
	 * of the md5 hash of the config password and the install_id (egw_admin_remote.remote_hash)
276
	 *
277
	 * @return string sussess message
278
	 * @throws Exception(lang('Invalid remote id or name "%1"!',$this->remote_id),997) or other Exceptions reported from remote
279
	 */
280
	protected function remote_exec()
281
	{
282
		if (!($remote = $this->read_remote($this->remote_id)))
283
		{
284
			throw new Api\Exception\WrongUserinput(lang('Invalid remote id or name "%1"!',$this->remote_id),997);
0 ignored issues
show
Unused Code introduced by
The call to lang() has too many arguments starting with $this->remote_id. ( Ignorable by Annotation )

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

284
			throw new Api\Exception\WrongUserinput(/** @scrutinizer ignore-call */ lang('Invalid remote id or name "%1"!',$this->remote_id),997);

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...
285
		}
286
		if (!$this->uid)
287
		{
288
			$this->save();	// to get the uid
289
		}
290
		$secret = md5($this->uid.$remote['remote_hash']);
291
292
		$postdata = $this->as_array();
293
		if (is_object($GLOBALS['egw']->translation))
294
		{
295
			$postdata = Api\Translation::convert($postdata,Api\Translation::charset(),'utf-8');
296
		}
297
		// dont send the id's which have no meaning on the remote install
298
		foreach(array('id','creator','modifier','requested','remote_id') as $name)
299
		{
300
			unset($postdata[$name]);
301
		}
302
		$opts = array('http' =>
303
		    array(
304
		        'method'  => 'POST',
305
		        'header'  => 'Content-type: application/x-www-form-urlencoded',
306
		        'content' => http_build_query($postdata),
307
		    )
308
		);
309
		$url = $remote['remote_url'].'/admin/remote.php?domain='.urlencode($remote['remote_domain']).'&secret='.urlencode($secret);
310
		//echo "sending command to $url\n"; _debug_array($opts);
311
		$http_response_header = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $http_response_header is dead and can be removed.
Loading history...
312
		if (!($message = @file_get_contents($url, false, stream_context_create($opts))))
313
		{
314
			throw new Api\Exception(lang('Could not remote execute the command').': '.$http_response_header[0]);
315
		}
316
		//echo "got: $message\n";
317
318
		if (($value = json_php_unserialize($message)) !== false && $message !== serialize(false))
0 ignored issues
show
introduced by
The condition $value = json_php_unserialize($message) !== false is always false.
Loading history...
319
		{
320
			$message = $value;
321
		}
322
		if (is_object($GLOBALS['egw']->translation))
323
		{
324
			$message = Api\Translation::convert($message,'utf-8');
325
		}
326
		$matches = null;
327
		if (is_string($message) && preg_match('/^([0-9]+) (.*)$/',$message,$matches))
328
		{
329
			throw new Api\Exception($matches[2],(int)$matches[1]);
330
		}
331
		return $message;
332
	}
333
334
	/**
335
	 * Delete / canncels a scheduled command
336
	 *
337
	 * @return boolean true on success, false otherwise
338
	 */
339
	function delete()
340
	{
341
		$this->cancel_periodic_job();
342
		if ($this->status != admin_cmd::scheduled) return false;
343
344
		$this->status = admin_cmd::deleted;
345
346
		return $this->save();
347
	}
348
349
	/**
350
	 * Saving the object to the database
351
	 *
352
	 * @param boolean $set_modifier =true set the current user as modifier or 0 (= run by the system)
353
	 * @return boolean true on success, false otherwise
354
	 */
355
	function save($set_modifier=true)
356
	{
357
		admin_cmd::_instanciate_sql();
358
359
		// check if uid already exists --> set the id to not try to insert it again (resulting in SQL error)
360
		if (!$this->id && $this->uid && (list($other) = self::$sql->search(array('cmd_uid' => $this->uid))))
361
		{
362
			$this->id = $other['id'];
0 ignored issues
show
Bug introduced by
The property id is declared read-only in admin_cmd.
Loading history...
363
		}
364
		if (!is_null($this->id))
0 ignored issues
show
introduced by
The condition is_null($this->id) is always false.
Loading history...
365
		{
366
			$this->modified = time();
0 ignored issues
show
Bug introduced by
The property modified is declared read-only in admin_cmd.
Loading history...
367
			$this->modifier = $set_modifier ? $GLOBALS['egw_info']['user']['account_id'] : 0;
0 ignored issues
show
Bug introduced by
The property modifier is declared read-only in admin_cmd.
Loading history...
368
			if ($set_modifier) $this->modifier_email = admin_cmd::user_email();
0 ignored issues
show
Bug introduced by
The property modifier_email is declared read-only in admin_cmd.
Loading history...
369
		}
370
		$vars = get_object_vars($this);	// does not work in php5.1.2 due a bug
371
372
		// data is stored serialized
373
		// paswords are masked / removed, if we dont need them anymore
374
		$vars['data'] = in_array($this->status, self::$require_pw_stati) ?
375
			json_encode($this->data) : self::mask_passwords($this->data);
376
377
		// skip EGroupware\\ prefix in new class-names, as value gets too long for column otherwise
378
		if (strpos($this->type, 'EGroupware\\') === 0)
379
		{
380
			$vars['type'] = substr($this->type, 11);
381
		}
382
383
		admin_cmd::$sql->init($vars);
384
		if (admin_cmd::$sql->save() != 0)
385
		{
386
			return false;
387
		}
388
		if (!$this->id)
389
		{
390
			$this->id = admin_cmd::$sql->data['id'];
391
			// if the cmd has no uid yet, we create one from our id and the install-id of this eGW instance
392
			if (!$this->uid)
393
			{
394
				$this->uid = $this->id.'-'.$GLOBALS['egw_info']['server']['install_id'];
0 ignored issues
show
Bug introduced by
The property uid is declared read-only in admin_cmd.
Loading history...
395
				admin_cmd::$sql->save(array('uid' => $this->uid));
396
			}
397
		}
398
		// install an async job, if we saved a scheduled job
399
		if ($this->status == admin_cmd::scheduled && empty($this->rrule))
400
		{
401
			admin_cmd::_set_async_job();
402
		}
403
		// schedule periodic execution, if we have an rrule
404
		elseif (!empty($this->rrule) && $this->status != admin_cmd::deleted)
405
		{
406
			$this->set_periodic_job();
407
		}
408
		// existing object with no rrule, cancle evtl. running periodic job
409
		elseif($vars['id'])
410
		{
411
			$this->cancel_periodic_job();
412
		}
413
		return true;
414
	}
415
416
	/**
417
	 * Mask / remove passwords in $data
418
	 *
419
	 * @param string|array $data json or php-encoded string or array
420
	 * @param boolean $return_serialized =true true: return json serialized string, false: return array
421
	 * @return string|array see $return_serialized
422
	 */
423
	static function mask_passwords($data, $return_serialized=true)
424
	{
425
		if (!is_array($data))
426
		{
427
			$data = json_php_unserialize($data);
428
		}
429
		foreach($data as $key => &$value)
430
		{
431
			if (is_array($value))
432
			{
433
				$value = self::mask_passwords($value, false);
434
			}
435
			elseif (preg_match('/(pw|passwd_?\d*|(?<!change)password|db_pass|secret)$/i', $key))
436
			{
437
				$value = str_repeat('*', strlen($value));
438
			}
439
		}
440
		return $return_serialized ? json_encode($data) : $data;
441
	}
442
443
	/**
444
	 * Reading a command from the queue returning the comand object
445
	 *
446
	 * @static
447
	 * @param int|string $id id or uid of the command
448
	 * @return admin_cmd or null if record not found
449
	 * @throws Api\Exception\WrongParameter if class does not exist or is no instance of admin_cmd
450
	 */
451
	static function read($id)
452
	{
453
		admin_cmd::_instanciate_sql();
454
455
		$keys = is_numeric($id) ? array('id' => $id) : array('uid' => $id);
456
		if (!($data = admin_cmd::$sql->read($keys)))
457
		{
458
			return $data;
459
		}
460
		return admin_cmd::instanciate($data);
461
	}
462
463
	/**
464
	 * Instanciated the object / subclass using the given data
465
	 *
466
	 * @static
467
	 * @param array $data
468
	 * @return admin_cmd
469
	 * @throws Api\Exception\WrongParameter if class does not exist or is no instance of admin_cmd
470
	 */
471
	static function instanciate(array $data)
472
	{
473
		if (isset($data['data']) && !is_array($data['data']))
474
		{
475
			$data['data'] = json_php_unserialize($data['data']);
476
		}
477
		if (!(class_exists($class = 'EGroupware\\'.$data['type']) ||	// namespaced class
478
			class_exists($class = $data['type'])) || $data['type'] == 'admin_cmd')
479
		{
480
			throw new Api\Exception\WrongParameter(lang('Unknown command %1!',$class), 10);
0 ignored issues
show
Unused Code introduced by
The call to lang() has too many arguments starting with $class. ( Ignorable by Annotation )

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

480
			throw new Api\Exception\WrongParameter(/** @scrutinizer ignore-call */ lang('Unknown command %1!',$class), 10);

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...
481
		}
482
		$cmd = new $class($data);
483
484
		if ($cmd instanceof admin_cmd)	// dont allow others classes to be executed that way!
485
		{
486
			return $cmd;
487
		}
488
		throw new Api\Exception\WrongParameter(lang('%1 is no command!',$class), 10);
489
	}
490
491
	/**
492
	 * calling get_rows of our static Api\Storage\Base instance
493
	 *
494
	 * @static
495
	 * @param array $query
496
	 * @param array &$rows
497
	 * @param array $readonlys
498
	 * @return int
499
	 */
500
	static function get_rows($query,&$rows,$readonlys)
501
	{
502
		admin_cmd::_instanciate_sql();
503
504
		if ((string)$query['col_filter']['remote_id'] === '0')
505
		{
506
			$query['col_filter']['remote_id'] = null;
507
		}
508
		if ((string)$query['col_filter']['periodic'] === '0')
509
		{
510
			$query['col_filter']['rrule'] = null;
511
		}
512
		else if ((string)$query['col_filter']['periodic'] === '1')
513
		{
514
			$query['col_filter'][] = 'cmd_rrule IS NOT NULL';
515
		}
516
		unset($query['col_filter']['periodic']);
517
		if($query['col_filter']['parent'])
518
		{
519
			$query['col_filter']['parent'] = (int)$query['col_filter']['parent'];
520
		}
521
522
		$total = admin_cmd::$sql->get_rows($query,$rows,$readonlys);
523
524
		if (!$rows) return 0;
525
526
		$async = new Api\Asyncservice();
527
		foreach($rows as &$row)
528
		{
529
			try {
530
				$cmd = admin_cmd::instanciate($row);
531
				$row['title'] = $cmd->__tostring();	// we call __tostring explicit, as a cast to string requires php5.2+
532
			}
533
			catch (Exception $e) {
534
				$row['title'] = $e->getMessage();
535
			}
536
537
			$row['value'] = $cmd->value;
0 ignored issues
show
Bug Best Practice introduced by
The property value does not exist on admin_cmd. Since you implemented __get, consider adding a @property annotation.
Loading history...
538
539
			if(method_exists($cmd, 'summary'))
540
			{
541
				$row['data'] = $cmd->summary();
542
			}
543
			else
544
			{
545
				$row['data'] = !($data = json_php_unserialize($row['data'])) ? '' :
546
					json_encode($data+(empty($row['rrule'])?array():array('rrule' => $row['rrule'])),
547
						JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
548
			}
549
			if($row['rrule'])
550
			{
551
				$rrule = calendar_rrule::event2rrule(calendar_rrule::parseRrule($row['rrule'],true)+array(
552
					'start' => time(),
553
					'tzid'=> Api\DateTime::$server_timezone->getName()
554
				));
555
				$row['rrule'] = ''.$rrule;
556
			}
557
			if(!$row['scheduled'] && $cmd && $cmd->async_job_id)
558
			{
559
				$job = $async->read($cmd->async_job_id);
560
561
				$row['scheduled'] = $job ? $job[$cmd->async_job_id]['next'] : null;
562
			}
563
			if ($row['status'] == admin_cmd::scheduled)
564
			{
565
				$row['class'] = 'AllowDelete';
566
			}
567
		}
568
		return $total;
569
	}
570
571
	/**
572
	 * Get list of stored or available (admin) cmd classes/types
573
	 *
574
	 * @return array class => label pairs
575
	 */
576
	static function get_cmd_labels()
577
	{
578
		return Api\Cache::getInstance(__CLASS__, 'cmd_labels', function()
579
		{
580
			admin_cmd::_instanciate_sql();
581
582
			// Need a new one to avoid column name modification
583
			$sql = new Api\Storage\Base('admin','egw_admin_queue',null);
584
			$labels = $sql->query_list('cmd_type');
585
586
			// for admin app we also add all available cmd objects
587
			foreach(scandir(__DIR__) as $file)
588
			{
589
				$matches = null;
590
				if (preg_match('/^class\.(admin_cmd_.*)\.inc\.php$/', $file, $matches))
591
				{
592
					if (!isset($labels[$matches[1]]))
593
					{
594
						$labels[$matches[1]] = $matches[1];
595
					}
596
				}
597
			}
598
			foreach($labels as $class => &$label)
599
			{
600
				if(class_exists($class))
601
				{
602
					$label = $class::name();
603
				}
604
				elseif (class_exists('EGroupware\\' . $class))
605
				{
606
					$class = 'EGroupware\\' . $class;
607
					$label = $class::name();
608
				}
609
			}
610
611
			// sort them alphabetic
612
			uasort($labels, function($a, $b)
613
			{
614
				return strcasecmp($a, $b);
615
			});
616
617
			return $labels;
618
		});
619
	}
620
621
	/**
622
	 * calling search method of our static Api\Storage\Base instance
623
	 *
624
	 * @static
625
	 * @param array|string $criteria array of key and data cols, OR a SQL query (content for WHERE), fully quoted (!)
626
	 * @param boolean|string|array $only_keys =true True returns only keys, False returns all cols. or
627
	 *	comma seperated list or array of columns to return
628
	 * @param string $order_by ='' fieldnames + {ASC|DESC} separated by colons ',', can also contain a GROUP BY (if it contains ORDER BY)
629
	 * @param string|array $extra_cols ='' string or array of strings to be added to the SELECT, eg. "count(*) as num"
630
	 * @param string $wildcard ='' appended befor and after each criteria
631
	 * @param boolean $empty =false False=empty criteria are ignored in query, True=empty have to be empty in row
632
	 * @param string $op ='AND' defaults to 'AND', can be set to 'OR' too, then criteria's are OR'ed together
633
	 * @param mixed $start =false if != false, return only maxmatch rows begining with start, or array($start,$num), or 'UNION' for a part of a union query
634
	 * @param array $filter =null if set (!=null) col-data pairs, to be and-ed (!) into the query without wildcards
635
	 * @return array
636
	 */
637
	static function &search($criteria,$only_keys=True,$order_by='',$extra_cols='',$wildcard='',$empty=False,$op='AND',$start=false,$filter=null)
638
	{
639
		admin_cmd::_instanciate_sql();
640
641
		return admin_cmd::$sql->search($criteria,$only_keys,$order_by,$extra_cols,$wildcard,$empty,$op,$start,$filter);
642
	}
643
644
	/**
645
	 * Instanciate our static Api\Storage\Base object for egw_admin_queue
646
	 *
647
	 * @static
648
	 */
649
	private static function _instanciate_sql()
650
	{
651
		if (is_null(admin_cmd::$sql))
652
		{
653
			admin_cmd::$sql = new Api\Storage\Base('admin','egw_admin_queue',null,'cmd_');
654
		}
655
	}
656
657
	/**
658
	 * Instanciate our static Api\Storage\Base object for egw_admin_remote
659
	 *
660
	 * @static
661
	 */
662
	private static function _instanciate_remote()
663
	{
664
		if (is_null(admin_cmd::$remote))
665
		{
666
			admin_cmd::$remote = new Api\Storage\Base('admin','egw_admin_remote');
667
		}
668
	}
669
670
	/**
671
	 * magic method to read a property, all non admin-cmd properties are stored in the data array
672
	 *
673
	 * @param string $property
674
	 * @return mixed
675
	 */
676
	function __get($property)
677
	{
678
		if (property_exists('admin_cmd',$property))
679
		{
680
			return $this->$property;	// making all (non static) class vars readonly available
681
		}
682
		switch($property)
683
		{
684
			case 'accounts':
685
				self::_instanciate_accounts();
686
				return self::$accounts;
687
			case 'data':
688
				return $this->data;
689
		}
690
		return $this->data[$property];
691
	}
692
693
	/**
694
	 * magic method to check if a property is set, all non admin-cmd properties are stored in the data array
695
	 *
696
	 * @param string $property
697
	 * @return boolean
698
	 */
699
	function __isset($property)
700
	{
701
		if (property_exists('admin_cmd',$property))
702
		{
703
			return isset($this->$property);	// making all (non static) class vars readonly available
704
		}
705
		return isset($this->data[$property]);
706
	}
707
708
	/**
709
	 * magic method to set a property, all non admin-cmd properties are stored in the data array
710
	 *
711
	 * @param string $property
712
	 * @param mixed $value
713
	 * @return mixed
714
	 */
715
	function __set($property,$value)
716
	{
717
		$this->data[$property] = $value;
718
	}
719
720
	/**
721
	 * magic method to unset a property, all non admin-cmd properties are stored in the data array
722
	 *
723
	 * @param string $property
724
	 */
725
	function __unset($property)
726
	{
727
		unset($this->data[$property]);
728
	}
729
730
	/**
731
	 * Return the whole object-data as array, it's a cast of the object to an array
732
	 *
733
	 * @return array
734
	 */
735
	function as_array()
736
	{
737
		if (version_compare(PHP_VERSION,'5.1.2','>'))
738
		{
739
			$vars = get_object_vars($this);	// does not work in php5.1.2 due a bug
740
		}
741
		else
742
		{
743
			foreach(array_keys(get_class_vars(__CLASS__)) as $name)
744
			{
745
				$vars[$name] = $this->$name;
746
			}
747
		}
748
		unset($vars['data']);
749
		if ($this->data) $vars = array_merge($this->data,$vars);
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->data 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...
750
751
		return $vars;
752
	}
753
754
	/**
755
	 * Check if the creator is still admin and has the neccessary admin rights
756
	 *
757
	 * @param string $extra_acl =null further admin rights to check, eg. 'account_access'
758
	 * @param int $extra_deny =null further admin rights to check, eg. 16 = deny edit Api\Accounts
759
	 * @throws Api\Exception\NoPermission\Admin
760
	 */
761
	protected function _check_admin($extra_acl=null,$extra_deny=null)
762
	{
763
		if ($this->creator)
764
		{
765
			admin_cmd::_instanciate_acl($this->creator);
0 ignored issues
show
Bug Best Practice introduced by
The method admin_cmd::_instanciate_acl() is not static, but was called statically. ( Ignorable by Annotation )

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

765
			admin_cmd::/** @scrutinizer ignore-call */ 
766
              _instanciate_acl($this->creator);
Loading history...
766
			// todo: check only if and with $this->creator
767
			if (!admin_cmd::$acl->check('run',1,'admin') &&		// creator is no longer admin
768
				$extra_acl && $extra_deny && admin_cmd::$acl->check($extra_acl,$extra_deny,'admin'))	// creator is explicitly forbidden to do something
0 ignored issues
show
Bug Best Practice introduced by
The expression $extra_deny of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
769
			{
770
				throw new Api\Exception\NoPermission\Admin();
771
			}
772
		}
773
	}
774
775
	/**
776
	 * parse application names, titles or localised names and return array of app-names
777
	 *
778
	 * @param array $apps names, titles or localised names
779
	 * @return array of app-names
780
	 * @throws Api\Exception\WrongUserinput lang("Application '%1' not found (maybe not installed or misspelled)!",$name),8
781
	 */
782
	static function parse_apps(array $apps)
783
	{
784
		foreach($apps as $key => $name)
785
		{
786
			if (!isset($GLOBALS['egw_info']['apps'][$name]))
787
			{
788
				foreach($GLOBALS['egw_info']['apps'] as $app => $data)	// check against title and localised name
789
				{
790
					if (!strcasecmp($name,$data['title']) || !strcasecmp($name,lang($app)))
791
					{
792
						$apps[$key] = $name = $app;
793
						break;
794
					}
795
				}
796
			}
797
			if (!isset($GLOBALS['egw_info']['apps'][$name]))
798
			{
799
				throw new Api\Exception\WrongUserinput(lang("Application '%1' not found (maybe not installed or misspelled)!",$name),8);
0 ignored issues
show
Unused Code introduced by
The call to lang() has too many arguments starting with $name. ( Ignorable by Annotation )

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

799
				throw new Api\Exception\WrongUserinput(/** @scrutinizer ignore-call */ lang("Application '%1' not found (maybe not installed or misspelled)!",$name),8);

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...
800
			}
801
		}
802
		return $apps;
803
	}
804
805
	/**
806
	 * parse account name or id
807
	 *
808
	 * @param string|int $account account_id or account_lid
809
	 * @param boolean $allow_only_user =null true=only user, false=only groups, default both
810
	 * @return int/array account_id
0 ignored issues
show
Documentation Bug introduced by
The doc comment int/array at position 0 could not be parsed: Unknown type name 'int/array' at position 0 in int/array.
Loading history...
811
	 * @throws Api\Exception\WrongUserinput(lang("Unknown account: %1 !!!",$account), 15);
812
	 * @throws Api\Exception\WrongUserinput(lang("Wrong account type: %1 is NO %2 !!!",$account,$allow_only_user?lang('user'):lang('group')), 16);
813
	 */
814
	static function parse_account($account,$allow_only_user=null)
815
	{
816
		admin_cmd::_instanciate_accounts();
817
818
		if (!($type = admin_cmd::$accounts->exists($account)) ||
819
			!is_numeric($id=$account) && !($id = admin_cmd::$accounts->name2id($account)))
820
		{
821
			throw new Api\Exception\WrongUserinput(lang("Unknown account: %1 !!!",$account), 15);
0 ignored issues
show
Unused Code introduced by
The call to lang() has too many arguments starting with $account. ( Ignorable by Annotation )

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

821
			throw new Api\Exception\WrongUserinput(/** @scrutinizer ignore-call */ lang("Unknown account: %1 !!!",$account), 15);

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...
822
		}
823
		if (!is_null($allow_only_user) && $allow_only_user !== ($type == 1))
824
		{
825
			throw new Api\Exception\WrongUserinput(lang("Wrong account type: %1 is NO %2 !!!",$account,$allow_only_user?lang('user'):lang('group')), 16);
826
		}
827
		if ($type == 2 && $id > 0) $id = -$id;	// groups use negative id's internally, fix it, if user given the wrong sign
828
829
		return $id;
830
	}
831
832
	/**
833
	 * parse account names or ids
834
	 *
835
	 * @param string|int|array $accounts array or comma-separated account_id's or account_lid's
836
	 * @param boolean $allow_only_user =null true=only user, false=only groups, default both
837
	 * @return array of account_id's or null if none specified
838
	 * @throws Api\Exception\WrongUserinput(lang("Unknown account: %1 !!!",$account), 15);
839
	 * @throws Api\Exception\WrongUserinput(lang("Wrong account type: %1 is NO %2 !!!",$account,$allow_only?lang('user'):lang('group')), 16);
840
	 */
841
	static function parse_accounts($accounts,$allow_only_user=null)
842
	{
843
		if (!$accounts) return null;
844
845
		$ids = array();
846
		foreach(is_array($accounts) ? $accounts : explode(',',$accounts) as $account)
847
		{
848
			$ids[] = admin_cmd::parse_account($account,$allow_only_user);
849
		}
850
		return $ids;
851
	}
852
853
	/**
854
	 * Parses a date into an integer timestamp
855
	 *
856
	 * @param string $date
857
	 * @return int timestamp
858
	 * @throws Api\Exception\WrongUserinput(lang('Invalid formated date "%1"!',$datein),6);
859
	 */
860
	static function parse_date($date)
861
	{
862
		if (!is_numeric($date))	// we allow to input a timestamp
863
		{
864
			$datein = $date;
865
			// convert german DD.MM.YYYY format into ISO YYYY-MM-DD format
866
			$date = preg_replace('/^([0-9]{1,2})\.([0-9]{1,2})\.([0-9]{4})$/','\3-\2-\1',$date);
867
868
			if (($date = strtotime($date))  === false)
869
			{
870
				throw new Api\Exception\WrongUserinput(lang('Invalid formated date "%1"!',$datein),6);
0 ignored issues
show
Unused Code introduced by
The call to lang() has too many arguments starting with $datein. ( Ignorable by Annotation )

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

870
				throw new Api\Exception\WrongUserinput(/** @scrutinizer ignore-call */ lang('Invalid formated date "%1"!',$datein),6);

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...
871
			}
872
		}
873
		return (int)$date;
874
	}
875
876
	/**
877
	 * Parse a boolean value
878
	 *
879
	 * @param string|boolean|int $value
880
	 * @param boolean $default =null
881
	 * @return boolean
882
	 * @throws Api\Exception\WrongUserinput(lang('Invalid value "%1" use yes or no!',$value),998);
883
	 */
884
	static function parse_boolean($value,$default=null)
885
	{
886
		if (is_bool($value) || is_int($value))
887
		{
888
			return (boolean)$value;
889
		}
890
		if (is_null($value) || (string)$value === '')
891
		{
892
			return $default;
893
		}
894
		if (in_array($value,array('1','yes','true',lang('yes'),lang('true'))))
895
		{
896
			return true;
897
		}
898
		if (in_array($value,array('0','no','false',lang('no'),lang('false'))))
899
		{
900
			return false;
901
		}
902
		throw new Api\Exception\WrongUserinput(lang('Invalid value "%1" use yes or no!',$value),998);
0 ignored issues
show
Unused Code introduced by
The call to lang() has too many arguments starting with $value. ( Ignorable by Annotation )

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

902
		throw new Api\Exception\WrongUserinput(/** @scrutinizer ignore-call */ lang('Invalid value "%1" use yes or no!',$value),998);

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...
903
	}
904
905
	/**
906
	 * Parse a remote id or name and return the remote_id
907
	 *
908
	 * @param string $id_or_name
909
	 * @return int remote_id
910
	 * @throws Api\Exception\WrongUserinput(lang('Invalid remote id or name "%1"!',$id_or_name),997);
911
	 */
912
	static function parse_remote($id_or_name)
913
	{
914
		admin_cmd::_instanciate_remote();
915
916
		if (!($remotes = admin_cmd::$remote->search(array(
917
			'remote_id' => $id_or_name,
918
			'remote_name' => $id_or_name,
919
			'remote_domain' => $id_or_name,
920
		),true,'','','',false,'OR')) || count($remotes) != 1)
921
		{
922
			throw new Api\Exception\WrongUserinput(lang('Invalid remote id or name "%1"!',$id_or_name),997);
0 ignored issues
show
Unused Code introduced by
The call to lang() has too many arguments starting with $id_or_name. ( Ignorable by Annotation )

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

922
			throw new Api\Exception\WrongUserinput(/** @scrutinizer ignore-call */ lang('Invalid remote id or name "%1"!',$id_or_name),997);

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...
923
		}
924
		return $remotes[0]['remote_id'];
925
	}
926
927
	/**
928
	 * Instanciated Api\Accounts class
929
	 *
930
	 * @todo Api\Accounts class instanciation for setup
931
	 * @throws Api\Exception\AssertionFailed(lang('%1 class not instanciated','accounts'),999);
932
	 */
933
	static function _instanciate_accounts()
934
	{
935
		if (!is_object(admin_cmd::$accounts))
936
		{
937
			if (!is_object($GLOBALS['egw']->accounts))
938
			{
939
				throw new Api\Exception\AssertionFailed(lang('%1 class not instanciated','accounts'),999);
0 ignored issues
show
Unused Code introduced by
The call to lang() has too many arguments starting with 'accounts'. ( Ignorable by Annotation )

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

939
				throw new Api\Exception\AssertionFailed(/** @scrutinizer ignore-call */ lang('%1 class not instanciated','accounts'),999);

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...
940
			}
941
			admin_cmd::$accounts = $GLOBALS['egw']->accounts;
942
		}
943
	}
944
945
	/**
946
	 * Instanciated Acl class
947
	 *
948
	 * @todo Acl class instanciation for setup
949
	 * @param int $account =null account_id the class needs to be instanciated for, default need only account-independent methods
950
	 * @throws Api\Exception\AssertionFailed(lang('%1 class not instanciated','acl'),999);
951
	 */
952
	protected function _instanciate_acl($account=null)
953
	{
954
		if (!is_object(admin_cmd::$acl) || $account && admin_cmd::$acl->account_id != $account)
0 ignored issues
show
Bug Best Practice introduced by
The expression $account of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
955
		{
956
			if (!is_object($GLOBALS['egw']->acl))
957
			{
958
				throw new Api\Exception\AssertionFailed(lang('%1 class not instanciated','acl'),999);
0 ignored issues
show
Unused Code introduced by
The call to lang() has too many arguments starting with 'acl'. ( Ignorable by Annotation )

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

958
				throw new Api\Exception\AssertionFailed(/** @scrutinizer ignore-call */ lang('%1 class not instanciated','acl'),999);

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...
959
			}
960
			if ($account && $GLOBALS['egw']->acl->account_id != $account)
0 ignored issues
show
Bug Best Practice introduced by
The expression $account of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
961
			{
962
				admin_cmd::$acl = new Acl($account);
963
				admin_cmd::$acl->read_repository();
964
			}
965
			else
966
			{
967
				admin_cmd::$acl = $GLOBALS['egw']->acl;
968
			}
969
		}
970
	}
971
972
	/**
973
	 * RFC822 email address of the an account, eg. "Ralf Becker <[email protected]>"
974
	 *
975
	 * @param $account_id =null account_id, default current user
0 ignored issues
show
Documentation Bug introduced by
The doc comment =null at position 0 could not be parsed: Unknown type name '=null' at position 0 in =null.
Loading history...
976
	 * @return string
977
	 */
978
	static function user_email($account_id=null)
979
	{
980
		if ($account_id)
981
		{
982
			admin_cmd::_instanciate_accounts();
983
			$fullname = admin_cmd::$accounts->id2name($account_id,'account_fullname');
984
			$email = admin_cmd::$accounts->id2name($account_id,'account_email');
985
		}
986
		else
987
		{
988
			$fullname = $GLOBALS['egw_info']['user']['account_fullname'];
989
			$email = $GLOBALS['egw_info']['user']['account_email'];
990
		}
991
		return $fullname . ($email ? ' <'.$email.'>' : '');
992
	}
993
994
	/**
995
	 * Semaphore to not permanently set new jobs, while we running the current ones
996
	 *
997
	 * @var boolean
998
	 */
999
	private static $running_queued_jobs = false;
1000
	const async_job_id = 'admin-command-queue';
0 ignored issues
show
Coding Style introduced by
This class constant is not uppercase (expected ASYNC_JOB_ID).
Loading history...
1001
1002
	/**
1003
	 * Setup an async job to run the next scheduled command
1004
	 *
1005
	 * Only needs to be called if a new command gets scheduled
1006
	 *
1007
	 * @return boolean true if job installed, false if not necessary
1008
	 */
1009
	private static function _set_async_job()
1010
	{
1011
		if (admin_cmd::$running_queued_jobs)
1012
		{
1013
			return false;
1014
		}
1015
		if (!($jobs = admin_cmd::search(array(),false,'cmd_scheduled','','',false,'AND',array(0,1),array(
1016
			'cmd_status' => admin_cmd::scheduled,
1017
		))))
1018
		{
1019
			return false;		// no schduled command, no need to setup the job
1020
		}
1021
		$next = $jobs[0];
1022
		if (($time = $next['scheduled']) < time())	// should run immediatly
1023
		{
1024
			return admin_cmd::run_queued_jobs();
1025
		}
1026
		$async = new Api\Asyncservice();
1027
1028
		// we cant use this class as callback, as it's abstract and ExecMethod used by the async service instanciated the class!
1029
		list($app) = explode('_',$class=$next['type']);
1030
		$callback = $app.'.'.$class.'.run_queued_jobs';
1031
1032
		$async->cancel_timer(admin_cmd::async_job_id);	// we delete it in case a job already exists
1033
		return $async->set_timer($time,admin_cmd::async_job_id,$callback,null,$next['creator']);
1034
	}
1035
1036
	/**
1037
	 * Callback for our async job
1038
	 *
1039
	 * @return boolean true if new job got installed, false if not necessary
1040
	 */
1041
	static function run_queued_jobs()
1042
	{
1043
		if (!($jobs = admin_cmd::search(array(),false,'cmd_scheduled','','',false,'AND',false,array(
1044
			'cmd_status' => admin_cmd::scheduled,
1045
			'cmd_scheduled <= '.time(),
1046
		))))
1047
		{
1048
			return false;		// no schduled commands, no need to setup the job
1049
		}
1050
		admin_cmd::$running_queued_jobs = true;	// stop admin_cmd::run() which calls admin_cmd::save() to install a new job
1051
1052
		foreach($jobs as $job)
1053
		{
1054
			try {
1055
				$cmd = admin_cmd::instanciate($job);
1056
				$cmd->run(null,false);	// false = dont set current user as modifier, as job is run by the queue/system itself
1057
			}
1058
			catch (Exception $e) {	// we need to mark that command as failed, to prevent further execution
1059
				_egw_log_exception($e);
1060
				admin_cmd::$sql->init($job);
1061
				admin_cmd::$sql->save(array(
1062
					'status' => admin_cmd::failed,
1063
					'error'  => $e->getMessage(),
1064
					'errno'  => $e->getcode(),
1065
					'data'   => self::mask_passwords($job['data']),
1066
				));
1067
			}
1068
		}
1069
		admin_cmd::$running_queued_jobs = false;
1070
1071
		return admin_cmd::_set_async_job();
1072
	}
1073
1074
	const PERIOD_ASYNC_ID_PREFIX = 'admin-cmd-';
1075
1076
	/**
1077
	 * Schedule next execution of a periodic job
1078
	 *
1079
	 * @return boolean
1080
	 */
1081
	public function set_periodic_job()
1082
	{
1083
		if (empty($this->rrule)) return false;
1084
1085
		// parse rrule and calculate next execution time
1086
		$event = calendar_rrule::parseRrule($this->rrule, true);	// true: allow HOURLY or MINUTELY
1087
		// rrule can depend on start-time, use policy creation time by default, if rrule_start is not set
1088
		$event['start'] = empty($this->rrule_start) ? $this->created : $this->rrule_start;
1089
		$event['tzid'] = Api\DateTime::$server_timezone->getName();
1090
		$rrule = calendar_rrule::event2rrule($event, false);	// false = server timezone
1091
		$rrule->rewind();
1092
		while((($time = $rrule->current()->format('ts'))) <= time())
1093
		{
1094
			$rrule->next();
1095
		}
1096
1097
		// schedule run_periodic_job to run at that time
1098
		$async = new Api\Asyncservice();
1099
		$job_id = empty($this->async_job_id) ? self::PERIOD_ASYNC_ID_PREFIX.$this->id : $this->async_job_id;
1100
		$async->cancel_timer($job_id);	// we delete it in case a job already exists
1101
		return $async->set_timer($time, $job_id, __CLASS__.'::run_periodic_job', $this->as_array(), $this->creator);
1102
	}
1103
1104
	/**
1105
	 * Cancel evtl. existing periodic job
1106
	 *
1107
	 * @return boolean true if job was canceled, false otherwise
1108
	 */
1109
	public function cancel_periodic_job()
1110
	{
1111
		$async = new Api\Asyncservice();
1112
		$job_id = empty($this->async_job_id) ? self::PERIOD_ASYNC_ID_PREFIX.$this->id : $this->async_job_id;
1113
		$async->cancel_timer($job_id);	// we delete it in case a job already exists
1114
	}
1115
1116
	/**
1117
	 * Run a periodic job, record it's result and schedule next run
1118
	 */
1119
	static function run_periodic_job($data)
1120
	{
1121
		$cmd = admin_cmd::read($data['id']);
1122
1123
		// schedule next execution
1124
		$cmd->set_periodic_job();
1125
1126
		// instanciate single periodic execution object
1127
		$single = $cmd->as_array();
1128
		$single['parent'] = $single['id'];
1129
		$args = array_diff_key($single, array_flip(array(
1130
			'id','uid',
1131
			'created','modified','modifier',
1132
			'async_job_id','rrule','scheduled',
1133
			'status', 'set', 'old','value','result'
1134
		)));
1135
1136
		$periodic = admin_cmd::instanciate($args);
1137
1138
		try {
1139
			$value = $periodic->run(null, false);
1140
		}
1141
		catch (Exception $ex) {
1142
			_egw_log_exception($ex);
1143
			error_log(__METHOD__."(".array2string($data).") periodic execution failed: ".$ex->getMessage());
1144
		}
1145
		$periodic->result = $value;
1146
		$periodic->save();
1147
	}
1148
1149
	/**
1150
	 * Return a list of defined remote instances
1151
	 *
1152
	 * @return array remote_id => remote_name pairs, plus 0 => local
1153
	 */
1154
	static function remote_sites()
1155
	{
1156
		admin_cmd::_instanciate_remote();
1157
1158
		$sites = array(lang('local'));
1159
		if (($remote = admin_cmd::$remote->query_list('remote_name','remote_id')))
1160
		{
1161
			$sites = array_merge($sites,$remote);
1162
		}
1163
		return $sites;
1164
	}
1165
1166
	/**
1167
	 * get_rows for remote instances
1168
	 *
1169
	 * @param array $query
1170
	 * @param array &$rows
1171
	 * @param array &$readonlys
1172
	 * @return int
1173
	 */
1174
	static function get_remotes($query,&$rows,&$readonlys)
1175
	{
1176
		admin_cmd::_instanciate_remote();
1177
1178
		return admin_cmd::$remote->get_rows($query,$rows,$readonlys);
1179
	}
1180
1181
	/**
1182
	 * Read data of a remote instance
1183
	 *
1184
	 * @param array|int $keys
1185
	 * @return array
1186
	 */
1187
	static function read_remote($keys)
1188
	{
1189
		admin_cmd::_instanciate_remote();
1190
1191
		return admin_cmd::$remote->read($keys);
1192
	}
1193
1194
	/**
1195
	 * Save / adds a remote instance
1196
	 *
1197
	 * @param array $data
1198
	 * @return int remote_id
1199
	 */
1200
	static function save_remote(array $data)
1201
	{
1202
		admin_cmd::_instanciate_remote();
1203
1204
		if ($data['install_id'] && $data['config_passwd'])	// calculate hash
1205
		{
1206
			$data['remote_hash'] = self::remote_hash($data['install_id'],$data['config_passwd']);
1207
		}
1208
		elseif (!$data['remote_hash'] && !($data['install_id'] && $data['config_passwd']))
1209
		{
1210
			throw new Api\Exception\WrongUserinput(lang('Either Install ID AND config password needed OR the remote hash!'));
1211
		}
1212
		//_debug_array($data);
1213
		admin_cmd::$remote->init($data);
1214
1215
		// check if a unique key constrain would be violated by saving the entry
1216
		if (($num = admin_cmd::$remote->not_unique()))
1217
		{
1218
			$col = admin_cmd::$remote->table_def['uc'][$num-1];	// $num is 1 based!
1219
			throw new egw_exception_db_not_unique(lang('Value for column %1 is not unique!',$this->table_name.'.'.$col),$num);
0 ignored issues
show
Unused Code introduced by
The call to lang() has too many arguments starting with $this->table_name . '.' . $col. ( Ignorable by Annotation )

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

1219
			throw new egw_exception_db_not_unique(/** @scrutinizer ignore-call */ lang('Value for column %1 is not unique!',$this->table_name.'.'.$col),$num);

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...
Bug Best Practice introduced by
The property table_name does not exist on admin_cmd. Since you implemented __get, consider adding a @property annotation.
Loading history...
Comprehensibility Best Practice introduced by
Using $this inside a static method is generally not recommended and can lead to errors in newer PHP versions.
Loading history...
Bug introduced by
The type egw_exception_db_not_unique was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
1220
		}
1221
		if (admin_cmd::$remote->save() != 0)
1222
		{
1223
			throw new Api\Db\Exception(lang('Error saving to db:').' '.$this->sql->db->Error.' ('.$this->sql->db->Errno.')',$this->sql->db->Errno);
0 ignored issues
show
Bug introduced by
The property db is declared protected in EGroupware\Api\Storage\Base and cannot be accessed from this context.
Loading history...
1224
		}
1225
		return admin_cmd::$remote->data['remote_id'];
1226
	}
1227
1228
	/**
1229
	 * Calculate the remote hash from install_id and config_passwd
1230
	 *
1231
	 * @param string $install_id
1232
	 * @param string $config_passwd
1233
	 * @return string 32char md5 hash
1234
	 */
1235
	static function remote_hash($install_id,$config_passwd)
1236
	{
1237
		if (empty($config_passwd) || !self::is_md5($install_id))
1238
		{
1239
			throw new Api\Exception\WrongParameter(empty($config_passwd)?'Empty Api\Config password':'install_id no md5 hash');
1240
		}
1241
		if (!self::is_md5($config_passwd)) $config_passwd = md5($config_passwd);
1242
1243
		return md5($config_passwd.$install_id);
1244
	}
1245
1246
	/**
1247
	 * displays an account specified by it's id or lid
1248
	 *
1249
	 * We show the value given by the user, plus the full name in brackets.
1250
	 *
1251
	 * @param int|string $account
1252
	 * @return string
1253
	 */
1254
	static function display_account($account)
1255
	{
1256
		$id = is_numeric($account) ? $account : $GLOBALS['egw']->accounts->id2name($account);
1257
1258
		return $account.' ('.Api\Accounts::username($id).')';
1259
	}
1260
1261
	/**
1262
	 * Check if string is a md5 hash (32 chars of 0-9 or a-f)
1263
	 *
1264
	 * @param string $str
1265
	 * @return boolean
1266
	 */
1267
	static function is_md5($str)
1268
	{
1269
		return preg_match('/^[0-9a-f]{32}$/',$str);
1270
	}
1271
1272
	/**
1273
	 * Check if the current command has the right crediential to be excuted remotely
1274
	 *
1275
	 * Command can reimplement that method, to allow eg. anonymous execution.
1276
	 *
1277
	 * This default implementation use a secret to authenticate with the installation,
1278
	 * which is a md5 hash build from the uid of the command (to not allow to send new
1279
	 * commands with an earsdroped secret) and the md5 hash of the md5 hash of the
1280
	 * Api\Config password and the install_id (egw_admin_remote.remote_hash)
1281
	 *
1282
	 * @param string $secret hash used to authenticate the command (
1283
	 * @param string $config_passwd of the current domain
1284
	 * @throws Api\Exception\NoPermission
1285
	 */
1286
	function check_remote_access($secret,$config_passwd)
1287
	{
1288
		// as a security measure remote administration need to be enabled under Admin > Site configuration
1289
		list(,$remote_admin_install_id) = explode('-',$this->uid);
1290
		$allowed_remote_admin_ids = $GLOBALS['egw_info']['server']['allow_remote_admin'] ? explode(',',$GLOBALS['egw_info']['server']['allow_remote_admin']) : array();
1291
1292
		// to authenticate with the installation we use a secret, which is a md5 hash build from the uid
1293
		// of the command (to not allow to send new commands with an earsdroped secret) and the md5 hash
1294
		// of the md5 hash of the Api\Config password and the install_id (egw_admin_remote.remote_hash)
1295
		if (is_null($config_passwd) || is_numeric($this->uid) || !in_array($remote_admin_install_id,$allowed_remote_admin_ids) ||
1296
			$secret != ($md5=md5($this->uid.$this->remote_hash($GLOBALS['egw_info']['server']['install_id'],$config_passwd))))
1297
		{
1298
			//die("secret='$secret' != '$md5', is_null($config_passwd)=".is_null($config_passwd).", uid=$this->uid, remote_install_id=$remote_admin_install_id, allowed: ".implode(', ',$allowed_remote_admin_ids));
1299
			unset($md5);
1300
			$msg = lang('Permission denied!');
1301
			if (!in_array($remote_admin_install_id,$allowed_remote_admin_ids))
1302
			{
1303
				$msg .= "\n".lang('Remote administration need to be enabled in the remote instance under Admin > Site configuration!');
1304
			}
1305
			throw new Api\Exception\NoPermission($msg,0);
1306
		}
1307
	}
1308
1309
	/**
1310
	 * Return a rand string, eg. to generate passwords
1311
	 *
1312
	 * @param int $len =16
1313
	 * @return string
1314
	 */
1315
	static function randomstring($len=16)
1316
	{
1317
		return Api\Auth::randomstring($len);
1318
	}
1319
1320
	/**
1321
	 * Get name of eTemplate used to make the change to derive UI for history
1322
	 *
1323
	 * @return string|null etemplate name
1324
	 */
1325
	protected function get_etemplate_name()
1326
	{
1327
		return null;
1328
	}
1329
1330
	/**
1331
	 * Return eTemplate used to make the change to derive UI for history
1332
	 *
1333
	 * @return Api\Etemplate|null
1334
	 */
1335
	protected function get_etemplate()
1336
	{
1337
		static $tpl = null;	// some caching to not instanciate it twice
1338
1339
		if (!isset($tpl))
1340
		{
1341
			$name = $this->get_etemplate_name();
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $name is correct as $this->get_etemplate_name() targeting admin_cmd::get_etemplate_name() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
1342
			if (empty($name))
1343
			{
1344
				$tpl = false;
1345
			}
1346
			else
1347
			{
1348
				$tpl = Api\Etemplate::instance($name);
1349
				Api\Etemplate::reset_request();
1350
			}
1351
		}
1352
		return $tpl ? $tpl : null;
1353
	}
1354
1355
	/**
1356
	 * Return (human readable) labels for keys of changes
1357
	 *
1358
	 * @return array
1359
	 */
1360
	function get_change_labels()
1361
	{
1362
		$labels = [];
1363
		$label = null;
1364
		if (($tpl = $this->get_etemplate()))
1365
		{
1366
			$tpl->run(function($cname, $expand, $widget) use (&$labels, &$label)
1367
			{
1368
				switch($widget->type)
1369
				{
1370
					// remember label from last description widget
1371
					case 'description':
1372
						if (!empty($widget->attrs['value'])) $label = $widget->attrs['value'];
1373
						break;
1374
					// ignore non input-widgets
1375
					case 'hbox': case 'vbox': case 'box': case 'groupbox':
1376
					case 'grid': case 'columns': case 'column': case 'rows': case 'row':
1377
					case 'template': case 'tabbox': case 'tabs': case 'tab':
1378
					// ignore buttons too
1379
					case 'button': case 'buttononly':
1380
						break;
1381
					default:
1382
						if (!empty($widget->id))
1383
						{
1384
							if (!empty($widget->attrs['label'])) $label = $widget->attrs['label'];
1385
							if (!empty($label)) $labels[$widget->id] = $label;
1386
							$label = null;
1387
						}
1388
						break;
1389
				}
1390
				unset($cname, $expand);
1391
			}, ['', []]);
1392
		}
1393
		return $labels;
1394
	}
1395
1396
	/**
1397
	 * Return widget types (indexed by field key) for changes
1398
	 *
1399
	 * Used by historylog widget to show the changes the command recorded.
1400
	 */
1401
	function get_change_widgets()
1402
	{
1403
		static $selectboxes = ['select', 'listbox', 'menupopup', 'taglist'];
1404
1405
		$widgets = [];
1406
		$last_select = null;
1407
		if (($tpl = $this->get_etemplate()))
1408
		{
1409
			$tpl->run(function($cname, $expand, $widget) use (&$widgets, &$last_select, $selectboxes)
1410
			{
1411
				switch($widget->type)
1412
				{
1413
					// ignore non input-widgets
1414
					case 'hbox': case 'vbox': case 'box': case 'groupbox':
1415
					case 'grid': case 'columns': case 'column': case 'rows': case 'row':
1416
					case 'template': case 'tabbox': case 'tabs': case 'tab':
1417
					// No need for these
1418
					case 'textbox': case 'int': case 'float':
1419
					// ignore widgets that can't go in the historylog
1420
					case 'button': case 'buttononly': case 'taglist-thumbnail':
1421
						break;
1422
					case 'radio':
1423
						if (!is_array($widgets[$widget->id])) $widgets[$widget->id] = [];
1424
						$label = (string)$widget->attrs['label'];
1425
						// translate "{something} {else}" type options
1426
						if (strpos($label, '{') !== false)
1427
						{
1428
							$label = preg_replace_callback('/{([^}]+)}/', function($matches)
1429
							{
1430
								return lang($matches[1]);
1431
							}, $label);
1432
						}
1433
						$widgets[$widget->id][(string)$widget->attrs['set_value']] = $label;
1434
						break;
1435
					// config templates have options in the template
1436
					case 'option':
1437
						if (!is_array($widgets[$last_select])) $widgets[$last_select] = [];
1438
						$label = (string)$widget->attrs['#text'];
1439
						// translate "{something} {else}" type options
1440
						if (strpos($label, '{') !== false)
1441
						{
1442
							$label = preg_replace_callback('/{([^}]+)}/', function($matches)
1443
							{
1444
								return lang($matches[1]);
1445
							}, $label);
1446
						}
1447
						$widgets[$last_select][(string)$widget->attrs['value']] = $label;
1448
						break;
1449
					default:
1450
						$last_select = null;
1451
						if (!empty($widget->id))
1452
						{
1453
							$widgets[$widget->id] = $widget->type;
1454
							if (in_array($widget->type, $selectboxes))
1455
							{
1456
								$last_select = $widget->id;
1457
							}
1458
						}
1459
						break;
1460
				}
1461
				unset($cname, $expand);
1462
			}, ['', []]);
1463
1464
			// remove pure selectboxes, as they would show nothing without having options
1465
			$widgets = array_diff($widgets, $selectboxes);
1466
		}
1467
		return $widgets;
1468
	}
1469
1470
	/**
1471
	 * Get the result of executing the command.
1472
	 * Should be some kind of success or results message indicating what was done.
1473
	 */
1474
	public function get_result()
1475
	{
1476
		if($this->result)
1477
		{
1478
			return is_array($this->result) ? implode("\n", $this->result) : $this->result;
1479
		}
1480
		return lang("Command was run %1 on %2",
1481
				static::$stati[ $this->status ],
0 ignored issues
show
Unused Code introduced by
The call to lang() has too many arguments starting with static::stati[$this->status]. ( Ignorable by Annotation )

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

1481
		return /** @scrutinizer ignore-call */ lang("Command was run %1 on %2",

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...
1482
				Api\DateTime::to($this->created));
1483
	}
1484
}
1485