Issues (4868)

admin/inc/class.admin_cmd.inc.php (51 issues)

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
This class constant is not uppercase (expected DELETED).
Loading history...
59
	const scheduled  = 1;
0 ignored issues
show
This class constant is not uppercase (expected SCHEDULED).
Loading history...
60
	const successful = 2;
0 ignored issues
show
This class constant is not uppercase (expected SUCCESSFUL).
Loading history...
61
	const failed     = 3;
0 ignored issues
show
This class constant is not uppercase (expected FAILED).
Loading history...
62
	const pending    = 4;
0 ignored issues
show
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
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
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
The property creator is declared read-only in admin_cmd.
Loading history...
189
		$this->creator_email = admin_cmd::user_email();
0 ignored issues
show
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
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
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
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
The property error is declared read-only in admin_cmd.
Loading history...
228
					$ret = $this->errno = $e->getCode();
0 ignored issues
show
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
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
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
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
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
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
The property id is declared read-only in admin_cmd.
Loading history...
363
		}
364
		if (!is_null($this->id))
0 ignored issues
show
The condition is_null($this->id) is always false.
Loading history...
365
		{
366
			$this->modified = time();
0 ignored issues
show
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
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
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
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
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
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
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
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
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
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
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
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
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
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...
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
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
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
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