infolog_bo::write()   F
last analyzed

Complexity

Conditions 117

Size

Total Lines 317
Code Lines 155

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 117
eloc 155
nop 8
dl 0
loc 317
rs 3.3333
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 - InfoLog - Business object
4
 *
5
 * @link http://www.egroupware.org
6
 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
7
 * @author Joerg Lehrke <[email protected]>
8
 * @package infolog
9
 * @copyright (c) 2003-17 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
10
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
11
 */
12
13
use EGroupware\Api;
14
use EGroupware\Api\Link;
15
use EGroupware\Api\Acl;
16
use EGroupware\Api\Vfs;
17
18
/**
19
 * This class is the BO-layer of InfoLog
20
 */
21
class infolog_bo
22
{
23
	/**
24
	 * Undelete right
25
	 */
26
	const ACL_UNDELETE = Acl::CUSTOM1;
27
28
	var $enums;
29
	var $status;
30
	/**
31
	 * Instance of our so class
32
	 *
33
	 * @var infolog_so
34
	 */
35
	var $so;
36
	/**
37
	 * Total from last search call
38
	 * @var int
39
	 */
40
	var $total;
41
	var $vfs;
42
	var $vfs_basedir='/infolog';
43
	/**
44
	 * Set Logging
45
	 *
46
	 * @var boolean
47
	 */
48
	var $log = false;
49
	/**
50
	 * Cached timezone data
51
	 *
52
	 * @var array id => data
53
	 */
54
	protected static $tz_cache = array();
55
	/**
56
	 * current time as timestamp in user-time and server-time
57
	 *
58
	 * @var int
59
	 */
60
	var $user_time_now;
61
	var $now;
62
	/**
63
	 * name of timestamps in an InfoLog entry
64
	 *
65
	 * @var array
66
	 */
67
	var $timestamps = array('info_startdate','info_enddate','info_datemodified','info_datecompleted','info_created');
68
	/**
69
	 * fields the responsible user can change
70
	 *
71
	 * @var array
72
	 */
73
	var $responsible_edit=array('info_status','info_percent','info_datecompleted');
74
	/**
75
	 * Fields to exclude from copy, if an entry is copied, the ones below are excluded by default.
76
	 *
77
	 * @var array
78
	 */
79
	var $copy_excludefields = array('info_id', 'info_uid', 'info_etag', 'caldav_name', 'info_created', 'info_creator', 'info_datemodified', 'info_modifier');
80
	/**
81
	 * Fields to exclude from copy, if a sub-entry is created, the ones below are excluded by default.
82
	 *
83
	 * @var array
84
	 */
85
	var $sub_excludefields = array('info_id', 'info_uid', 'info_etag', 'caldav_name', 'info_created', 'info_creator', 'info_datemodified', 'info_modifier');
86
	/**
87
	 * Additional fields to $sub_excludefields to exclude, if no config stored
88
	 *
89
	 * @var array
90
	 */
91
	var $default_sub_excludefields = array('info_des');
92
	/**
93
	 * implicit ACL rights of the responsible user: read or edit
94
	 *
95
	 * @var string
96
	 */
97
	var $implicit_rights='read';
98
	/**
99
	 * Custom fields read from the infolog config
100
	 *
101
	 * @var array
102
	 */
103
	var $customfields=array();
104
	/**
105
	 * Group owners for certain types read from the infolog config
106
	 *
107
	 * @var array
108
	 */
109
	var $group_owners=array();
110
	/**
111
	 * Current user
112
	 *
113
	 * @var int
114
	 */
115
	var $user;
116
	/**
117
	 * History loggin: ''=no, 'history'=history & delete allowed, 'history_admin_delete', 'history_no_delete'
118
	 *
119
	 * @var string
120
	 */
121
	var $history;
122
	/**
123
	 * Instance of infolog_tracking, only instaciated if needed!
124
	 *
125
	 * @var infolog_tracking
126
	 */
127
	var $tracking;
128
	/**
129
	 * Maximum number of line characters (-_+=~) allowed in a mail, to not stall the layout.
130
	 * Longer lines / biger number of these chars are truncated to that max. number or chars.
131
	 *
132
	 * @var int
133
	 */
134
	var $max_line_chars = 40;
135
136
	/**
137
	 * Available filters
138
	 *
139
	 * @var array filter => label pairs
140
	 */
141
	var $filters = array(
142
		''                     => 'no Filter',
143
		'done'                     => 'done',
144
		'responsible'              => 'responsible',
145
		'responsible-open-today'   => 'responsible open',
146
		'responsible-open-overdue' => 'responsible overdue',
147
		'responsible-upcoming'     => 'responsible upcoming',
148
		'responsible-open-upcoming'=> 'responsible open and upcoming',
149
		'delegated'                => 'delegated',
150
		'delegated-open-today'     => 'delegated open',
151
		'delegated-open-overdue'   => 'delegated overdue',
152
		'delegated-upcoming'       => 'delegated upcomming',
153
		'delegated-open-upcoming'  => 'delegated open and upcoming',
154
		'own'                      => 'own',
155
		'own-open-today'           => 'own open',
156
		'own-open-overdue'         => 'own overdue',
157
		'own-upcoming'             => 'own upcoming',
158
		'own-open-upcoming'		   => 'own open and upcoming',
159
		'private'                  => 'private',
160
		'open-today'               => 'open(status)',
161
		'open-overdue'             => 'overdue',
162
		'upcoming'                 => 'upcoming',
163
		'open-upcoming'			   => 'open and upcoming',
164
		'bydate'                   => 'startdate',
165
		'duedate'                  => 'enddate'
166
	);
167
168
	/**
169
	 * Constructor Infolog BO
170
	 *
171
	 * @param int $info_id
172
	 */
173
	function __construct($info_id = 0)
174
	{
175
		$this->enums = $this->stock_enums = array(
0 ignored issues
show
Bug Best Practice introduced by
The property stock_enums does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
176
			'priority' => array (
177
				3 => 'urgent',
178
				2 => 'high',
179
				1 => 'normal',
180
				0 => 'low'
181
			),
182
			'confirm'   => array(
183
				'not' => 'not','accept' => 'accept','finish' => 'finish',
184
				'both' => 'both' ),
185
			'type'      => array(
186
				'task' => 'task','phone' => 'phone','note' => 'note','email' => 'email'
187
			/*	,'confirm' => 'confirm','reject' => 'reject','fax' => 'fax' not implemented so far */ )
188
		);
189
		$this->status = $this->stock_status = array(
0 ignored issues
show
Bug Best Practice introduced by
The property stock_status does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
190
			'defaults' => array(
191
				'task' => 'not-started', 'phone' => 'not-started', 'note' => 'done','email' => 'done'),
192
			'task' => array(
193
				'offer' => 'offer',				// -->  NEEDS-ACTION
194
				'not-started' => 'not-started',	// iCal NEEDS-ACTION
195
				'ongoing' => 'ongoing',			// iCal IN-PROCESS
196
				'done' => 'done',				// iCal COMPLETED
197
				'cancelled' => 'cancelled',		// iCal CANCELLED
198
				'billed' => 'billed',			// -->  DONE
199
				'template' => 'template',		// -->  cancelled
200
				'nonactive' => 'nonactive',		// -->  cancelled
201
				'archive' => 'archive' ), 		// -->  cancelled
202
			'phone' => array(
203
				'not-started' => 'call',		// iCal NEEDS-ACTION
204
				'ongoing' => 'will-call',		// iCal IN-PROCESS
205
				'done' => 'done', 				// iCal COMPLETED
206
				'billed' => 'billed' ),			// -->  DONE
207
			'note' => array(
208
				'ongoing' => 'ongoing',			// iCal has no status on notes
209
				'done' => 'done' ),
210
			'email' => array(
211
				'ongoing' => 'ongoing',			// iCal has no status on notes
212
				'done' => 'done' ),
213
		);
214
		if (($config_data = Api\Config::read('infolog')))
215
		{
216
			$this->allow_past_due_date = $config_data['allow_past_due_date'] === null ? 1 : $config_data['allow_past_due_date'];
0 ignored issues
show
Bug Best Practice introduced by
The property allow_past_due_date does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
217
			if (isset($config_data['status']) && is_array($config_data['status']))
218
			{
219
				foreach(array_keys($config_data['status']) as $key)
220
				{
221
					if (!is_array($this->status[$key]))
222
					{
223
						$this->status[$key] = array();
224
					}
225
					$this->status[$key] = array_merge($this->status[$key],(array)$config_data['status'][$key]);
226
				}
227
			}
228
			if (isset($config_data['types']) && is_array($config_data['types']))
229
			{
230
				//echo "stock-types:<pre>"; print_r($this->enums['type']); echo "</pre>\n";
231
				//echo "config-types:<pre>"; print_r($config_data['types']); echo "</pre>\n";
232
				$this->enums['type'] += $config_data['types'];
233
				//echo "types:<pre>"; print_r($this->enums['type']); echo "</pre>\n";
234
			}
235
			if ($config_data['group_owners']) $this->group_owners = $config_data['group_owners'];
236
237
			$this->customfields = Api\Storage\Customfields::get('infolog');
238
			if ($this->customfields)
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->customfields 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...
239
			{
240
				foreach($this->customfields as $name => $field)
241
				{
242
					// old infolog customefield record
243
					if(empty($field['type']))
244
					{
245
						if (count($field['values'])) $field['type'] = 'select'; // selectbox
246
						elseif ($field['rows'] > 1) $field['type'] = 'textarea'; // textarea
247
						elseif (intval($field['len']) > 0) $field['type'] = 'text'; // regular input field
248
						else $field['type'] = 'label'; // header-row
249
						$field['type2'] = $field['typ'];
250
						unset($field['typ']);
251
						$this->customfields[$name] = $field;
252
						$save_config = true;
253
					}
254
				}
255
				if ($save_config) Api\Config::save_value('customfields',$this->customfields,'infolog');
256
			}
257
			if (is_array($config_data['responsible_edit']))
258
			{
259
				$this->responsible_edit = array_merge($this->responsible_edit,$config_data['responsible_edit']);
260
			}
261
			if (is_array($config_data['copy_excludefields']))
262
			{
263
				$this->copy_excludefields = array_merge($this->copy_excludefields,$config_data['copy_excludefields']);
264
			}
265
			if (is_array($config_data['sub_excludefields']) && $config_data['sub_excludefields'])
266
			{
267
				$this->sub_excludefields = array_merge($this->sub_excludefields,$config_data['sub_excludefields']);
268
			}
269
			else
270
			{
271
				$this->sub_excludefields = array_merge($this->sub_excludefields,$this->default_sub_excludefields);
272
			}
273
			if ($config_data['implicit_rights'] == 'edit')
274
			{
275
				$this->implicit_rights = 'edit';
276
			}
277
			$this->history = $config_data['history'];
278
		}
279
		// sort types by there translation
280
		foreach($this->enums['type'] as $key => $val)
281
		{
282
			if (($val = lang($key)) != $key.'*') $this->enums['type'][$key] = lang($key);
0 ignored issues
show
Unused Code introduced by
The assignment to $val is dead and can be removed.
Loading history...
283
		}
284
		natcasesort($this->enums['type']);
285
286
		$this->user = $GLOBALS['egw_info']['user']['account_id'];
287
288
		$this->now = time();
289
		$this->user_time_now = Api\DateTime::server2user($this->now,'ts');
0 ignored issues
show
Documentation Bug introduced by
It seems like EGroupware\Api\DateTime:...2user($this->now, 'ts') of type EGroupware\Api\datetime is incompatible with the declared type integer of property $user_time_now.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
290
291
		$this->grants = $GLOBALS['egw']->acl->get_grants('infolog',$this->group_owners ? $this->group_owners : true);
0 ignored issues
show
Bug Best Practice introduced by
The property grants does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
292
		$this->so = new infolog_so($this->grants);
293
294
		if ($info_id)
295
		{
296
			$this->read( $info_id );
297
		}
298
		else
299
		{
300
			$this->init();
301
		}
302
	}
303
304
	/**
305
	 * checks if there are customfields for typ $typ
306
	 *
307
	 * @param string $type
308
	 * @return boolean True if there are customfields for $typ, else False
309
	 */
310
	function has_customfields($type)
311
	{
312
		foreach($this->customfields as $field)
313
		{
314
			if ((!$type || empty($field['type2']) || in_array($type,is_array($field['type2']) ? $field['type2'] : explode(',',$field['type2']))))
315
			{
316
				return True;
317
			}
318
		}
319
		return False;
320
	}
321
322
	/**
323
	 * check's if user has the requiered rights on entry $info_id
324
	 *
325
	 * @param int|array $info data or info_id of infolog entry to check
326
	 * @param int $required_rights ACL::{READ|EDIT|ADD|DELETE}|infolog_bo::ACL_UNDELETE
327
	 * @param int $other uid to check (if info==0) or 0 to check against $this->user
328
	 * @param int $user = null user whos rights to check, default current user
329
	 * @return boolean
330
	 */
331
	function check_access($info,$required_rights,$other=0,$user=null)
332
	{
333
		static $cache = array();
334
335
		$info_id = is_array($info) ? $info['info_id'] : $info;
336
337
		if (!$user) $user = $this->user;
0 ignored issues
show
Bug Best Practice introduced by
The expression $user 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...
338
		if ($user == $this->user)
339
		{
340
			$grants = $this->grants;
341
			if ($info_id) $access =& $cache[$info_id][$required_rights];	// we only cache the current user!
342
		}
343
		else
344
		{
345
			$grants = $GLOBALS['egw']->acl->get_grants('infolog',$this->group_owners ? $this->group_owners : true,$user);
346
		}
347
		if (!$info)
348
		{
349
			$owner = $other ? $other : $user;
350
			$grant = $grants[$owner];
351
			return $grant & $required_rights;
352
		}
353
354
355
		if (!isset($access))
356
		{
357
			// handle delete for the various history modes
358
			if ($this->history)
359
			{
360
				if (!is_array($info) && !($info = $this->so->read(array('info_id' => $info_id)))) return false;
361
362
				if ($info['info_status'] == 'deleted' &&
363
					($required_rights == Acl::EDIT ||		// no edit rights for deleted entries
364
					 $required_rights == Acl::ADD  ||		// no add rights for deleted entries
365
					 $required_rights == Acl::DELETE && ($this->history == 'history_no_delete' || // no delete at all!
366
					 $this->history == 'history_admin_delete' && (!isset($GLOBALS['egw_info']['user']['apps']['admin']) || $user!=$this->user))))	// delete only for admins
367
				{
368
					$access = false;
369
				}
370
				elseif ($required_rights == self::ACL_UNDELETE)
371
				{
372
					if ($info['info_status'] != 'deleted')
373
					{
374
						$access = false;	// can only undelete deleted items
375
					}
376
					else
377
					{
378
						// undelete requires edit rights
379
						$access = $this->so->check_access( $info,Acl::EDIT,$this->implicit_rights == 'edit',$grants,$user );
380
					}
381
				}
382
			}
383
			elseif ($required_rights == self::ACL_UNDELETE)
384
			{
385
				$access = false;
386
			}
387
			if (!isset($access))
388
			{
389
				$access = $this->so->check_access( $info,$required_rights,$this->implicit_rights == 'edit',$grants,$user );
390
			}
391
		}
392
		// else $cached = ' (from cache)';
393
		// error_log(__METHOD__."($info_id,$required_rights,$other,$user) returning$cached ".array2string($access));
394
		return $access;
395
	}
396
397
	/**
398
	 * Check if user is responsible for an entry: he or one of his memberships is in responsible
399
	 *
400
	 * @param array $info infolog entry as array
401
	 * @return boolean
402
	 */
403
	function is_responsible($info)
404
	{
405
		return $this->so->is_responsible($info);
406
	}
407
408
	/**
409
	 * init internal data to be empty
410
	 */
411
	function init()
412
	{
413
		$this->so->init();
414
	}
415
416
	/**
417
	 * convert a link_id value into an info_from text
418
	 *
419
	 * @param array &$info infolog entry, key info_from gets set by this function
420
	 * @param string $not_app = '' app to exclude
421
	 * @param string $not_id = '' id to exclude
422
	 * @return boolean True if we have a linked item, False otherwise
423
	 */
424
	function link_id2from(&$info,$not_app='',$not_id='')
425
	{
426
		//error_log(__METHOD__ . "(subject='{$info['info_subject']}', link_id='{$info['info_link_id']}', from='{$info['info_from']}', not_app='$not_app', not_id='$not_id')");
427
428
		if ($info['info_link_id'] > 0 &&
429
			(isset($info['links']) && ($link = $info['links'][$info['info_link_id']]) ||	// use supplied links info
430
			 ($link = Link::get_link($info['info_link_id'])) !== False))	// if link not found in supplied links, we always search!
431
		{
432
			if (isset($info['links']) && isset($link['app']))
433
			{
434
				$app = $link['app'];
435
				$id  = $link['id'];
436
			}
437
			else
438
			{
439
				$nr = $link['link_app1'] == 'infolog' && $link['link_id1'] == $info['info_id'] ? '2' : '1';
440
				$app = $link['link_app'.$nr];
441
				$id  = $link['link_id'.$nr];
442
			}
443
			$title = Link::title($app,$id);
444
445
			if ((string)$info['info_custom_from'] === '')	// old entry
446
			{
447
				$info['info_custom_from'] = (int) ($title != $info['info_from'] && @htmlentities($title) != $info['info_from']);
448
			}
449
			if (!$info['info_custom_from'])
450
			{
451
				$info['info_from'] = '';
452
				$info['info_custom_from'] = 0;
453
			}
454
			if ($app == $not_app && $id == $not_id)
455
			{
456
				return False;
457
			}
458
			// if link is a project and no other project selected, also add as project
459
			if ($app == 'projectmanager' && $id && !$info['pm_id'])
460
			{
461
				$info['old_pm_id'] = $info['pm_id'] = $id;
462
			}
463
			else
464
			{
465
				// Link might be contact, check others
466
				$this->get_pm_id($info);
467
			}
468
			$info['info_link'] = $info['info_contact'] = array(
469
				'app'   => $app,
470
				'id'    => $id,
471
				'title' => (!empty($info['info_from']) ? $info['info_from'] : $title),
472
			);
473
474
			//echo " title='$title'</p>\n";
475
			return $info['blur_title'] = $title;
476
		}
477
478
		// Set ID to 'none' instead of unset to make it seem like there's a value
479
		$info['info_link'] = $info['info_contact'] = $info['info_from'] ? array('id' => 'none', 'title' => $info['info_from']) : null;
480
		$info['info_link_id'] = 0;	// link might have been deleted
481
		$info['info_custom_from'] = (int)!!$info['info_from'];
482
483
		$this->get_pm_id($info);
484
485
		return False;
486
	}
487
488
	/**
489
	 * Find projectmanager ID from linked project(s)
490
	 *
491
	 * @param Array $info
492
	 */
493
	public function get_pm_id(&$info)
494
	{
495
		$pm_links = Link::get_links('infolog',$info['info_id'],'projectmanager');
496
497
		$old_pm_id = is_array($pm_links) ? array_shift($pm_links) : $info['old_pm_id'];
0 ignored issues
show
introduced by
The condition is_array($pm_links) is always true.
Loading history...
498
		if (!isset($info['pm_id']) && $old_pm_id) $info['pm_id'] = $old_pm_id;
499
		return $old_pm_id;
500
	}
501
502
	/**
503
	 * Create a subject from a description: truncate it and add ' ...'
504
	 */
505
	static function subject_from_des($des)
506
	{
507
		return substr($des,0,60).' ...';
508
	}
509
510
	/**
511
	 * Convert the timestamps from given timezone to another and keep dates.
512
	 * The timestamps are mostly expected to be in server-time
513
	 * and $fromTZId is only used to qualify dates.
514
	 *
515
	 * @param array $values to modify
516
	 * @param string $fromTZId = null
517
	 * @param string $toTZId = false
518
	 * 		TZID timezone name e.g. 'UTC'
519
	 * 			or NULL for timestamps in user-time
520
	 * 			or false for timestamps in server-time
521
	 */
522
	 function time2time(&$values, $fromTZId=false, $toTZId=null)
523
	 {
524
525
		if ($fromTZId === $toTZId) return;
526
527
		$tz = Api\DateTime::$server_timezone;
528
529
	 	if ($fromTZId)
530
		{
531
			if (!isset(self::$tz_cache[$fromTZId]))
532
			{
533
				self::$tz_cache[$fromTZId] = calendar_timezones::DateTimeZone($fromTZId);
534
			}
535
			$fromTZ = self::$tz_cache[$fromTZId];
536
		}
537
		elseif (is_null($fromTZId))
0 ignored issues
show
introduced by
The condition is_null($fromTZId) is always false.
Loading history...
538
		{
539
			$tz = Api\DateTime::$user_timezone;
540
			$fromTZ = Api\DateTime::$user_timezone;
541
		}
542
		else
543
		{
544
			$fromTZ = Api\DateTime::$server_timezone;
545
		}
546
		if ($toTZId)
547
		{
548
			if (!isset(self::$tz_cache[$toTZId]))
549
			{
550
				self::$tz_cache[$toTZId] = calendar_timezones::DateTimeZone($toTZId);
551
			}
552
			$toTZ = self::$tz_cache[$toTZId];
553
		}
554
		elseif (is_null($toTZId))
555
		{
556
			$toTZ = Api\DateTime::$user_timezone;
557
		}
558
		else
559
		{
560
			$toTZ = Api\DateTime::$server_timezone;
561
		}
562
		//error_log(__METHOD__.'(values[info_enddate]='.date('Y-m-d H:i:s',$values['info_enddate']).", from=".array2string($fromTZId).", to=".array2string($toTZId).") tz=".$tz->getName().', fromTZ='.$fromTZ->getName().', toTZ='.$toTZ->getName().', userTZ='.Api\DateTime::$user_timezone->getName());
563
	 	foreach($this->timestamps as $key)
564
		{
565
		 	if ($values[$key])
566
		 	{
567
			 	$time = new Api\DateTime($values[$key], $tz);
568
			 	$time->setTimezone($fromTZ);
569
			 	if ($time->format('Hi') == '0000')
570
			 	{
571
				 	// we keep dates the same in new timezone
572
				 	$arr = Api\DateTime::to($time,'array');
573
				 	$time = new Api\DateTime($arr, $toTZ);
574
			 	}
575
			 	else
576
			 	{
577
				 	$time->setTimezone($toTZ);
578
			 	}
579
			 	$values[$key] = Api\DateTime::to($time,'ts');
580
		 	}
581
		}
582
		//error_log(__METHOD__.'() --> values[info_enddate]='.date('Y-m-d H:i:s',$values['info_enddate']));
583
	 }
584
585
	/**
586
	 * convert a date from server to user-time
587
	 *
588
	 * @param int $ts timestamp in server-time
589
	 * @param string $date_format = 'ts' date-formats: 'ts'=timestamp, 'server'=timestamp in server-time, 'array'=array or string with date-format
590
	 * @return mixed depending of $date_format
591
	 */
592
	function date2usertime($ts,$date_format='ts')
593
	{
594
		if (empty($ts) || $date_format == 'server') return $ts;
595
596
		return Api\DateTime::server2user($ts,$date_format);
597
	}
598
599
	/**
600
	 * Read an infolog entry specified by $info_id
601
	 *
602
	 * @param int|array $info_id integer id or array with id's or array with column=>value pairs of the entry to read
603
	 * @param boolean $run_link_id2from = true should link_id2from run, default yes,
604
	 *	need to be set to false if called from link-title to prevent an infinit recursion
605
	 * @param string $date_format = 'ts' date-formats: 'ts'=timestamp, 'server'=timestamp in server-time,
606
	 * 	'array'=array or string with date-format
607
	 * @param boolean $ignore_acl = false if true, do NOT check access, default false
608
	 *
609
	 * @return array|boolean infolog entry, null if not found or false if no permission to read it
610
	 */
611
	function &read($info_id,$run_link_id2from=true,$date_format='ts',$ignore_acl=false)
612
	{
613
		//error_log(__METHOD__.'('.array2string($info_id).', '.array2string($run_link_id2from).", '$date_format') ".function_backtrace());
614
		if (is_scalar($info_id) || isset($info_id[count($info_id)-1]))
615
		{
616
			if (is_scalar($info_id) && !is_numeric($info_id))
0 ignored issues
show
introduced by
The condition is_numeric($info_id) is always true.
Loading history...
617
			{
618
				$info_id = array('info_uid' => $info_id);
619
			}
620
			else
621
			{
622
				$info_id = array('info_id' => $info_id);
623
			}
624
		}
625
626
		if (!$info_id || ($data = $this->so->read($info_id)) === False)
627
		{
628
			return null;
629
		}
630
631
		if (!$ignore_acl && !$this->check_access($data,Acl::READ))	// check behind read, to prevent a double read
632
		{
633
			return False;
634
		}
635
636
		if ($data['info_subject'] == $this->subject_from_des($data['info_des']))
637
		{
638
			$data['info_subject'] = '';
639
		}
640
		if ($run_link_id2from)
641
		{
642
			$this->link_id2from($data);
643
		}
644
		// convert server- to user-time
645
		if ($date_format == 'ts')
646
		{
647
			$this->time2time($data);
648
649
			// pre-cache title and file access
650
			self::set_link_cache($data);
0 ignored issues
show
Bug Best Practice introduced by
The method infolog_bo::set_link_cache() 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

650
			self::/** @scrutinizer ignore-call */ 
651
         set_link_cache($data);
Loading history...
651
		}
652
653
		return $data;
654
	}
655
656
	/**
657
	 * Delete an infolog entry, evtl. incl. it's children / subs
658
	 *
659
	 * @param int|array $info_id int id
660
	 * @param boolean $delete_children should the children be deleted
661
	 * @param int|boolean $new_parent parent to use for not deleted children if > 0
662
	 * @param boolean $skip_notification Do not send notification of delete
663
	 * @return boolean True if delete was successful, False otherwise ($info_id does not exist or no rights)
664
	 */
665
	function delete($info_id,$delete_children=False,$new_parent=False, $skip_notification=False)
666
	{
667
		if (is_array($info_id))
668
		{
669
			$info_id = (int)(isset($info_id[0]) ? $info_id[0] : (isset($info_id['info_id']) ? $info_id['info_id'] : $info_id['info_id']));
670
		}
671
		if (($info = $this->so->read(array('info_id' => $info_id), true, 'server')) === False)
0 ignored issues
show
Unused Code introduced by
The call to infolog_so::read() has too many arguments starting with true. ( Ignorable by Annotation )

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

671
		if (($info = $this->so->/** @scrutinizer ignore-call */ read(array('info_id' => $info_id), true, 'server')) === False)

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...
672
		{
673
			return False;
674
		}
675
		if (!$this->check_access($info,Acl::DELETE))
676
		{
677
			return False;
678
		}
679
		// check if we have children and delete or re-parent them
680
		if (($children = $this->so->get_children($info_id)))
681
		{
682
			foreach($children as $id => $owner)
683
			{
684
				if ($delete_children && $this->so->grants[$owner] & Acl::DELETE)
685
				{
686
					$this->delete($id,$delete_children,$new_parent,$skip_notification);	// call ourself recursive to delete the child
687
				}
688
				else	// dont delete or no rights to delete the child --> re-parent it
689
				{
690
					$this->so->write(array(
691
						'info_id' => $id,
692
						'info_parent_id' => $new_parent,
693
					));
694
				}
695
			}
696
		}
697
		$deleted = $info;
698
		$deleted['info_status'] = 'deleted';
699
		$deleted['info_datemodified'] = time();
700
		$deleted['info_modifier'] = $this->user;
701
702
		// if we have history switched on and not an already deleted item --> set only status deleted
703
		if ($this->history && $info['info_status'] != 'deleted')
704
		{
705
			if ($info['info_status'] == 'deleted') return false;	// entry already deleted
706
707
			$this->so->write($deleted);
708
709
			Link::unlink(0,'infolog',$info_id,'','!file','',true);	// keep the file attachments, hide the rest
710
		}
711
		else
712
		{
713
			$this->so->delete($info_id,false);	// we delete the children via bo to get all notifications!
714
715
			Link::unlink(0,'infolog',$info_id);
716
		}
717
		if ($info['info_status'] != 'deleted')	// dont notify of final purge of already deleted items
718
		{
719
			// send email notifications and do the history logging
720
			if(!$skip_notification)
721
			{
722
				if (!is_object($this->tracking))
723
				{
724
					$this->tracking = new infolog_tracking($this);
725
				}
726
				$this->tracking->track($deleted,$info,$this->user,true);
727
			}
728
		}
729
		return True;
730
	}
731
732
	/**
733
	* writes the given $values to InfoLog, a new entry gets created if info_id is not set or 0
734
	*
735
	* checks and asures ACL
736
	*
737
	* @param array &$values values to write
738
	* @param boolean $check_defaults = true check and set certain defaults
739
	* @param boolean|int $touch_modified = true touch the modification date and sets the modifier's user-id, 2: only modifier
740
	* @param boolean $user2server = true conversion between user- and server-time necessary
741
	* @param boolean $skip_notification = false true = do NOT send notification, false (default) = send notifications
742
	* @param boolean $throw_exception = false Throw an exception (if required fields are not set)
743
	* @param string $purge_cfs = null null=dont, 'ical'=only iCal X-properties (cfs name starting with "#"), 'all'=all cfs
744
	* @param boolean $ignore_acl =true
745
	*
746
	* @return int|boolean info_id on a successfull write or false
747
	*/
748
	function write(&$values_in, $check_defaults=true, $touch_modified=true, $user2server=true,
749
		$skip_notification=false, $throw_exception=false, $purge_cfs=null, $ignore_acl=false)
750
	{
751
		$values = $values_in;
752
		//echo "boinfolog::write()values="; _debug_array($values);
753
		if (!$ignore_acl && (!$values['info_id'] && !$this->check_access(0,Acl::EDIT,$values['info_owner']) &&
754
			!$this->check_access(0,Acl::ADD,$values['info_owner'])))
755
		{
756
			return false;
757
		}
758
		// we need to get the old values to update the links in customfields and for the tracking
759
		if ($values['info_id'])
760
		{
761
			$old = $this->read($values['info_id'], false, 'server', $ignore_acl);
762
		}
763
764
		if (($status_only = !$ignore_acl && $values['info_id'] && !$this->check_access($values,Acl::EDIT)))
765
		{
766
			if (!isset($values['info_responsible']))
767
			{
768
				$responsible = $old['info_responsible'];
769
			}
770
			else
771
			{
772
				$responsible = $values['info_responsible'];
773
			}
774
			if (!($status_only = in_array($this->user, (array)$responsible)))	// responsible has implicit right to change status
775
			{
776
				$status_only = !!array_intersect((array)$responsible,array_keys($GLOBALS['egw']->accounts->memberships($this->user)));
777
			}
778
			if (!$status_only && $values['info_status'] != 'deleted')
779
			{
780
				$status_only = $undelete = $this->check_access($values['info_id'],self::ACL_UNDELETE);
781
			}
782
		}
783
		if (!$ignore_acl && ($values['info_id'] && !$this->check_access($values['info_id'],Acl::EDIT) && !$status_only ||
784
		    !$values['info_id'] && $values['info_id_parent'] && !$this->check_access($values['info_id_parent'],Acl::ADD)))
785
		{
786
			return false;
787
		}
788
789
		// Make sure status is still valid if the type changes
790
		if($old['info_type'] != $values['info_type'] && $values['info_status'])
791
		{
792
			if (isset($this->status[$values['info_type']]) &&
793
				!in_array($values['info_status'], array_keys($this->status[$values['info_type']])))
794
			{
795
				$values['info_status'] = $this->status['defaults'][$values['info_type']];
796
			}
797
		}
798
		if ($status_only && !$undelete)	// make sure only status gets writen
799
		{
800
			$set_completed = !$values['info_datecompleted'] &&	// set date completed of finished job, only if its not already set
801
				in_array($values['info_status'],array('done','billed','cancelled'));
802
803
			$values = $old;
804
			// only overwrite explicitly allowed fields
805
			$values['info_datemodified'] = $values_in['info_datemodified'];
806
			foreach ($this->responsible_edit as $name)
807
			{
808
				if (isset($values_in[$name])) $values[$name] = $values_in[$name];
809
			}
810
			if ($set_completed)
811
			{
812
				$values['info_datecompleted'] = $user2server ? $this->user_time_now : $this->now;
813
				$values['info_percent'] = 100;
814
				$forcestatus = true;
815
				$status = 'done';
816
				if (isset($values['info_type']) && !in_array($values['info_status'],array('done','billed','cancelled'))) {
817
					$forcestatus = false;
818
					//echo "set_completed:"; _debug_array($this->status[$values['info_type']]);
819
					if (isset($this->status[$values['info_type']]['done'])) {
820
						$forcestatus = true;
821
						$status = 'done';
822
					} elseif (isset($this->status[$values['info_type']]['billed'])) {
823
						$forcestatus = true;
824
						$status = 'billed';
825
					} elseif (isset($this->status[$values['info_type']]['cancelled'])) {
826
						$forcestatus = true;
827
						$status = 'cancelled';
828
					}
829
				}
830
				if ($forcestatus && !in_array($values['info_status'],array('done','billed','cancelled'))) $values['info_status'] = $status;
831
			}
832
			$check_defaults = false;
833
		}
834
		if ($check_defaults)
835
		{
836
			if (!$values['info_datecompleted'] &&
837
				(in_array($values['info_status'],array('done','billed'))))
838
			{
839
				$values['info_datecompleted'] = $user2server ? $this->user_time_now : $this->now;	// set date completed to today if status == done
840
			}
841
			// Check for valid status / percent combinations
842
			if (in_array($values['info_status'],array('done','billed')))
843
			{
844
				$values['info_percent'] = 100;
845
			}
846
			else if (in_array($values['info_status'], array('not-started')))
847
			{
848
				$values['info_percent'] = 0;
849
			}
850
			else if (($values['info_status'] != 'archive' && $values['info_status'] != 'cancelled' &&
851
				isset($this->stock_status[$values['info_type']]) && in_array($values['info_status'], $this->stock_status[$values['info_type']])) &&
852
				((int)$values['info_percent'] == 100 || $values['info_percent'] == 0))
853
			{
854
				// We change percent to match status, not status to match percent
855
				$values['info_percent'] = 10;
856
			}
857
			if ((int)$values['info_percent'] == 100 && !in_array($values['info_status'],array('done','billed','cancelled','archive')))
858
			{
859
				//echo "check_defaults:"; _debug_array($this->status[$values['info_type']]);
860
				//$values['info_status'] = 'done';
861
				$status = 'done';
862
				if (isset($values['info_type'])) {
863
					if (isset($this->status[$values['info_type']]['done'])) {
864
						$status = 'done';
865
					} elseif (isset($this->status[$values['info_type']]['billed'])) {
866
						$status = 'billed';
867
					} elseif (isset($this->status[$values['info_type']]['cancelled'])) {
868
						$status = 'cancelled';
869
					} else {
870
						// since the comlete stati above do not exist for that type, dont change it
871
						$status = $values['info_status'];
872
					}
873
				}
874
				$values['info_status'] = $status;
875
			}
876
			if ($values['info_responsible'] && $values['info_status'] == 'offer')
877
			{
878
				$values['info_status'] = 'not-started';   // have to match if not finished
879
			}
880
			if (isset($values['info_subject']) && empty($values['info_subject']))
881
			{
882
				$values['info_subject'] = $this->subject_from_des($values['info_des']);
883
			}
884
885
			// Check required custom fields
886
			if($throw_exception)
887
			{
888
				$custom = Api\Storage\Customfields::get('infolog');
889
				foreach($custom as $c_name => $c_field)
890
				{
891
					if($c_field['type2']) $type2 = is_array($c_field['type2']) ? $c_field['type2'] : explode(',',$c_field['type2']);
892
					if($c_field['needed'] && (!$c_field['type2'] || $c_field['type2'] && in_array($values['info_type'],$type2)))
893
					{
894
						// Required custom field
895
						if(!$values['#'.$c_name])
896
						{
897
							throw new Api\Exception\WrongUserinput(lang('For infolog type %1, %2 is required',lang($values['info_type']),$c_field['label']));
0 ignored issues
show
Unused Code introduced by
The call to lang() has too many arguments starting with lang($values['info_type']). ( Ignorable by Annotation )

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

897
							throw new Api\Exception\WrongUserinput(/** @scrutinizer ignore-call */ lang('For infolog type %1, %2 is required',lang($values['info_type']),$c_field['label']));

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...
898
						}
899
					}
900
				}
901
			}
902
		}
903
		if (isset($this->group_owners[$values['info_type']]))
904
		{
905
			$values['info_owner'] = $this->group_owners[$values['info_type']];
906
			if (!$ignore_acl && !($this->grants[$this->group_owners[$values['info_type']]] & Acl::EDIT))
907
			{
908
				if (!$this->check_access($values['info_id'],Acl::EDIT) ||
909
					!$values['info_id'] && !$this->check_access($values,Acl::ADD)
910
				)
911
				{
912
					return false;	// no edit rights from the group-owner and no implicit rights (delegated and sufficient rights)
913
				}
914
			}
915
			$values['info_access'] = 'public';	// group-owners are allways public
916
		}
917
		elseif (!$values['info_id'] && !$values['info_owner'] || $GLOBALS['egw']->accounts->get_type($values['info_owner']) == 'g')
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: (! $values['info_id'] &&...s['info_owner']) == 'g', Probably Intended Meaning: ! $values['info_id'] && ...['info_owner']) == 'g')
Loading history...
918
		{
919
			$values['info_owner'] = $this->so->user;
920
		}
921
922
		$to_write = $values;
923
		if ($user2server)
924
		{
925
			// convert user- to server-time
926
			$this->time2time($to_write, null, false);
927
		}
928
		else
929
		{
930
			// convert server- to user-time
931
			$this->time2time($values);
932
		}
933
934
		if ($touch_modified && $touch_modified !== 2 || !$values['info_datemodified'])
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($touch_modified && $tou...es['info_datemodified'], Probably Intended Meaning: $touch_modified && ($tou...s['info_datemodified'])
Loading history...
935
		{
936
			// Should only an entry be updated which includes the original modification date?
937
			// Used in the web-GUI to check against a modification by an other user while editing the entry.
938
			// It's now disabled for xmlrpc, as otherwise the xmlrpc code need to be changed!
939
			$xmlrpc = is_object($GLOBALS['server']) && $GLOBALS['server']->last_method;
940
			$check_modified = $values['info_datemodified'] && !$xmlrpc ? $to_write['info_datemodified'] : false;
941
			$values['info_datemodified'] = $this->user_time_now;
942
			$to_write['info_datemodified'] = $this->now;
943
		}
944
		if ($touch_modified || !$values['info_modifier'])
945
		{
946
			$values['info_modifier'] = $to_write['info_modifier'] = $this->so->user;
947
		}
948
949
		// set created and creator for new entries
950
		if (!$values['info_id'])
951
		{
952
			$values['info_created'] = $this->user_time_now;
953
			$to_write['info_created'] = $this->now;
954
			$values['info_creator'] = $to_write['info_creator'] = $this->so->user;
955
		}
956
		//_debug_array($values);
957
		// error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n".array2string($values)."\n",3,'/tmp/infolog');
958
959
		if (($info_id = $this->so->write($to_write, $check_modified, $purge_cfs, !isset($old))))
960
		{
961
			if (!isset($values['info_type']) || $status_only || empty($values['caldav_url']))
962
			{
963
				$values = $this->read($info_id, true, 'server', $ignore_acl);
964
			}
965
966
			$values['info_id'] = $info_id;
967
			$to_write['info_id'] = $info_id;
968
969
			// if the info responbsible array is not passed, fetch it from old.
970
			if (!array_key_exists('info_responsible',$values)) $values['info_responsible'] = $old['info_responsible'];
971
			if (!is_array($values['info_responsible']))		// this should not happen, bug it does ;-)
972
			{
973
				$values['info_responsible'] = $values['info_responsible'] ? explode(',',$values['info_responsible']) : array();
974
				$to_write['info_responsible'] = $values['info_responsible'];
975
			}
976
977
			// writing links for a new entry
978
			if (!$old && is_array($to_write['link_to']['to_id']) && count($to_write['link_to']['to_id']))
979
			{
980
				//echo "<p>writing links for new entry $info_id</p>\n"; _debug_array($content['link_to']['to_id']);
981
				Link::link('infolog',$info_id,$to_write['link_to']['to_id']);
982
				$values['link_to']['to_id'] = $info_id;
983
			}
984
			$this->write_check_links($to_write);
985
			if(!$values['info_link_id'] || $values['info_link_id'] != $to_write['info_link_id'])
986
			{
987
				// Just got a link ID, need to save it
988
				$this->so->write($to_write);
989
				$values['info_link_id'] = $to_write['info_link_id'];
990
				$values['info_contact'] = $to_write['info_contact'];
991
				$values['info_from'] = $to_write['info_from'];
992
				$this->link_id2from($values);
993
			}
994
			$values['pm_id'] = $to_write['pm_id'];
995
996
			if (($info_from_set = ($values['info_link_id'] && isset($values['info_from']) && empty($values['info_from']))))
997
			{
998
				$values['info_from'] = $to_write['info_from'] = $this->link_id2from($values);
999
			}
1000
1001
			// create (and remove) links in custom fields
1002
			if(!is_array($old))
1003
			{
1004
				$old = array();
1005
			}
1006
			Api\Storage\Customfields::update_links('infolog',$values,$old,'info_id');
1007
1008
			// Check for restore of deleted entry, restore held links
1009
			if($old['info_status'] == 'deleted' && $values['info_status'] != 'deleted')
1010
			{
1011
				Link::restore('infolog', $info_id);
1012
			}
1013
1014
			// notify the link-class about the update, as other apps may be subscribt to it
1015
			Link::notify_update('infolog',$info_id,$values);
1016
1017
			// pre-cache the new values
1018
			self::set_link_cache($values);
0 ignored issues
show
Bug Best Practice introduced by
The method infolog_bo::set_link_cache() 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

1018
			self::/** @scrutinizer ignore-call */ 
1019
         set_link_cache($values);
Loading history...
1019
1020
			// send email notifications and do the history logging
1021
			if (!is_object($this->tracking))
1022
			{
1023
				$this->tracking = new infolog_tracking($this);
1024
			}
1025
1026
			if ($old && ($missing_fields = array_diff_key($old,$values)))
1027
			{
1028
				// Some custom fields (multiselect with nothing selected) will be missing,
1029
				// and that's OK.  Don't put them back.
1030
				foreach(array_keys($missing_fields) as $field)
1031
				{
1032
					if(array_key_exists($field, $values_in))
1033
					{
1034
						unset($missing_fields[$field]);
1035
					}
1036
				}
1037
				$values = array_merge($values,$missing_fields);
1038
			}
1039
			// Add keys missing in the $to_write array
1040
			if (($missing_fields = array_diff_key($values,$to_write)))
1041
			{
1042
				$to_write = array_merge($to_write,$missing_fields);
1043
			}
1044
			$this->tracking->track($to_write,$old,$this->user,$values['info_status'] == 'deleted' || $old['info_status'] == 'deleted',
1045
				null,$skip_notification);
1046
1047
			if ($info_from_set) $values['info_from'] = '';
1048
1049
			// Change new values back to user time before sending them back
1050
			if($user2server)
1051
			{
1052
				$this->time2time($values);
1053
			}
1054
			// merge changes (keeping extra values from the UI)
1055
			$values_in = array_merge($values_in,$values);
1056
1057
			// Update modified timestamp of parent
1058
			if($values['info_id_parent'] && $touch_modified)
1059
			{
1060
				$parent = $this->read($values['info_id_parent'], true, 'server', true);
1061
				$this->write($parent, false, true, false, true, false, null, $ignore_acl);
1062
			}
1063
		}
1064
		return $info_id;
1065
	}
1066
1067
	/**
1068
	 * Check links when writing an infolog entry
1069
	 *
1070
	 * Checks for info_contact properly linked, project properly linked and
1071
	 * adds or removes to correct.
1072
	 *
1073
	 * @param Array $values
1074
	 */
1075
	protected function write_check_links(&$values)
1076
	{
1077
		$old_link_id = (int)$values['info_link_id'];
1078
		$from = $values['info_from'];
1079
1080
		if($values['info_contact'] && !(
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($values['info_contact']...values['info_contact']), Probably Intended Meaning: $values['info_contact'] ...alues['info_contact']))
Loading history...
1081
				is_array($values['info_contact']) && $values['info_contact']['id'] == 'none'
1082
			) || (
1083
				is_array($values['info_contact']) && $values['info_contact']['id'] == 'none' &&
1084
				array_key_exists('search', $values['info_contact'])
1085
		))
1086
		{
1087
			if(is_array($values['info_contact']))
1088
			{
1089
				// eTemplate2 returns the array all ready
1090
				$app = $values['info_contact']['app'];
1091
				$id = (int)$values['info_contact']['id'];
1092
				$from = $values['info_contact']['search'];
1093
			}
1094
			else if ($values['info_contact'])
1095
			{
1096
				list($app, $id) = explode(':', $values['info_contact'], 2);
1097
			}
1098
			// if project has been removed, but is still info_contact --> also remove it
1099
			if ($app == 'projectmanager' && $id && $id == $values['old_pm_id'] && !$values['pm_id'])
1100
			{
1101
				unset($values['info_link_id'], $id, $values['info_contact']);
1102
			}
1103
			else if ($app && $id)
1104
			{
1105
				if(!is_array($values['link_to']))
1106
				{
1107
					$values['link_to'] = array();
1108
				}
1109
				$values['info_link_id'] = (int)($info_link_id = Link::link(
0 ignored issues
show
Unused Code introduced by
The assignment to $info_link_id is dead and can be removed.
Loading history...
1110
						'infolog',
1111
						$values['info_id'],
1112
						$app,$id
1113
				));
1114
				$values['info_from'] = Link::title($app, $id);
1115
				if($values['pm_id'])
1116
				{
1117
					// They just changed the contact, don't clear the project
1118
					unset($old_link_id);
1119
				}
1120
			}
1121
			else if ($from)
1122
			{
1123
				$values['info_from'] = $from;
1124
			}
1125
			else
1126
			{
1127
				unset($values['info_link_id']);
1128
				$values['info_from'] = null;
1129
			}
1130
		}
1131
		else if ($values['pm_id'] && $values['info_id'] && !$values['old_pm_id'])
1132
		{
1133
			// Set for new entry with no contact
1134
			$app = 'projectmanager';
1135
			$id = $values['pm_id'];
1136
			$values['info_link_id'] = (int)($info_link_id = Link::link(
1137
				'infolog',
1138
				$values['info_id'],
1139
				$app,$id
1140
			));
1141
		}
1142
		else
1143
		{
1144
			unset($values['info_link_id']);
1145
			unset($values['info_contact']);
1146
			$values['info_from'] = $from ? $from : null;
1147
		}
1148
		if($values['info_id'] && $values['old_pm_id'] !== $values['pm_id'])
1149
		{
1150
			Link::unlink(0,'infolog',$values['info_id'],0,'projectmanager',$values['old_pm_id']);
1151
			// Project has changed, but link is not to project
1152
			if($values['pm_id'])
1153
			{
1154
				$link_id = Link::link('infolog', $values['info_id'], 'projectmanager', $values['pm_id']);
1155
				if(!$values['info_link_id'])
1156
				{
1157
					$values['info_link_id'] = $link_id;
1158
				}
1159
			}
1160
			else
1161
			{
1162
				// Project removed, but primary link is not to project
1163
				$values['pm_id'] = null;
1164
			}
1165
		}
1166
		if ($old_link_id && $old_link_id != $values['info_link_id'])
1167
		{
1168
			$link = Link::get_link($old_link_id);
1169
			// remove selected project, if removed link is that project
1170
			if($link['link_app2'] == 'projectmanager' && $link['link_id2'] == $values['old_pm_id'])
1171
			{
1172
				unset($values['pm_id'], $values['old_pm_id']);
1173
			}
1174
			Link::unlink($old_link_id);
1175
		}
1176
		// if linked to a project and no other project selected, also add as project
1177
		$links = Link::get_links('infolog', $values['info_id'], 'projectmanager');
1178
		if (!$values['pm_id'] && count($links))
1179
		{
1180
			$values['old_pm_id'] = $values['pm_id'] = array_pop($links);
1181
		}
1182
	}
1183
1184
	/**
1185
	 * Query the number of children / subs for one or more info_id's
1186
	 *
1187
	 * @param int|array $info_id id
1188
	 * @return int|array number of subs
1189
	 */
1190
	function anzSubs( $info_id )
1191
	{
1192
		return $this->so->anzSubs( $info_id );
1193
	}
1194
1195
	/**
1196
	 * searches InfoLog for a certain pattern in $query
1197
	 *
1198
	 * @param $query[order] column-name to sort after
0 ignored issues
show
Documentation Bug introduced by
The doc comment column-name at position 0 could not be parsed: Unknown type name 'column-name' at position 0 in column-name.
Loading history...
1199
	 * @param $query[sort] sort-order DESC or ASC
1200
	 * @param $query[filter] string with combination of acl-, date- and status-filters, eg. 'own-open-today' or ''
1201
	 * @param $query[cat_id] category to use or 0 or unset
1202
	 * @param $query[search] pattern to search, search is done in info_from, info_subject and info_des
1203
	 * @param $query[action] / $query[action_id] if only entries linked to a specified app/entry show be used
1204
	 * @param &$query[start], &$query[total] nextmatch-parameters will be used and set if query returns less entries
1205
	 * @param $query[col_filter] array with column-name - data pairs, data == '' means no filter (!)
1206
	 * @param boolean $no_acl =false true: ignore all acl
1207
	 * @return array with id's as key of the matching log-entries
1208
	 */
1209
	function &search(&$query, $no_acl=false)
1210
	{
1211
		//error_log(__METHOD__.'('.array2string($query).')');
1212
1213
		if($query['filter'] == 'bydate')
1214
		{
1215
			if (is_int($query['startdate'])) $query['col_filter'][] = 'info_startdate >= '.$GLOBALS['egw']->db->quote($query['startdate']);
1216
			if (is_int($query['enddate'])) $query['col_filter'][] = 'info_startdate <= '.$GLOBALS['egw']->db->quote($query['enddate']+(60*60*24)-1);
1217
		}
1218
		elseif ($query['filter'] == 'duedate')
1219
		{
1220
			if (is_int($query['startdate'])) $query['col_filter'][] = 'info_enddate >= '.$GLOBALS['egw']->db->quote($query['startdate']);
1221
			if (is_int($query['enddate'])) $query['col_filter'][] = 'info_enddate <= '.$GLOBALS['egw']->db->quote($query['enddate']+(60*60*24)-1);
1222
		}
1223
		elseif ($query['filter'] == 'private')
1224
		{
1225
			$query['col_filter'][] = 'info_access = ' . $GLOBALS['egw']->db->quote('private');
1226
		}
1227
		if (!isset($query['date_format']) || $query['date_format'] != 'server')
1228
		{
1229
			if (isset($query['col_filter']))
1230
			{
1231
				foreach ($this->timestamps as $key)
1232
				{
1233
					if (!empty($query['col_filter'][$key]))
1234
					{
1235
						$query['col_filter'][$key] = Api\DateTime::user2server($query['col_filter'][$key],'ts');
1236
					}
1237
				}
1238
			}
1239
		}
1240
1241
		$ret = $this->so->search($query, $no_acl);
1242
		$this->total = $query['total'];
1243
1244
		if (is_array($ret))
0 ignored issues
show
introduced by
The condition is_array($ret) is always true.
Loading history...
1245
		{
1246
			foreach ($ret as $id => &$data)
1247
			{
1248
				if (!$no_acl && !$this->check_access($data,Acl::READ))
1249
				{
1250
					unset($ret[$id]);
1251
					continue;
1252
				}
1253
				// convert system- to user-time
1254
				foreach ($this->timestamps as $key)
1255
				{
1256
					if ($data[$key])
1257
					{
1258
						$time = new Api\DateTime($data[$key], Api\DateTime::$server_timezone);
1259
						if (!isset($query['date_format']) || $query['date_format'] != 'server')
1260
						{
1261
							if ($time->format('Hi') == '0000')
1262
							{
1263
								// we keep dates the same in user-time
1264
								$arr = Api\DateTime::to($time,'array');
1265
								$time = new Api\DateTime($arr, Api\DateTime::$user_timezone);
1266
							}
1267
							else
1268
							{
1269
								$time->setTimezone(Api\DateTime::$user_timezone);
1270
							}
1271
						}
1272
						$data[$key] = Api\DateTime::to($time,'ts');
1273
					}
1274
				}
1275
				// pre-cache title and file access
1276
				self::set_link_cache($data);
0 ignored issues
show
Bug Best Practice introduced by
The method infolog_bo::set_link_cache() 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

1276
				self::/** @scrutinizer ignore-call */ 
1277
          set_link_cache($data);
Loading history...
1277
			}
1278
		}
1279
		//echo "<p>boinfolog::search(".print_r($query,True).")=<pre>".print_r($ret,True)."</pre>\n";
1280
		return $ret;
1281
	}
1282
1283
	/**
1284
	 * Query ctag for infolog
1285
	 *
1286
	 * @param array $filter = array('filter'=>'own','info_type'=>'task')
1287
	 * @return string
1288
	 */
1289
	public function getctag(array $filter=array('filter'=>'own','info_type'=>'task'))
1290
	{
1291
		$filter += array(
1292
			'order'			=> 'info_datemodified',
1293
			'sort'			=> 'DESC',
1294
			'date_format'	=> 'server',
1295
			'start'			=> 0,
1296
			'num_rows'		=> 1,
1297
		);
1298
		// we need to query deleted entries too for a ctag!
1299
		$filter['filter'] .= '+deleted';
1300
1301
		$result =& $this->search($filter);
1302
1303
		if (empty($result)) return 'EGw-empty-wGE';
1304
1305
		$entry = array_shift($result);
1306
1307
		return $entry['info_datemodified'];
1308
	}
1309
1310
	/**
1311
	 * imports a mail identified by uid as infolog
1312
	 *
1313
	 * @author Cornelius Weiss <[email protected]>
1314
	 * @todo search if infolog with from and subject allready exists ->appned body & inform user
1315
	 * @param array $_addresses array of addresses
1316
	 *	- array (email,name)
1317
	 * @param string $_subject
1318
	 * @param string $_message
1319
	 * @param array $_attachments
1320
	 * @param string $_date
1321
	 * @return array $content array for uiinfolog
1322
	 */
1323
	function import_mail($_addresses,$_subject,$_message,$_attachments,$_date)
1324
	{
1325
		foreach($_addresses as $address)
1326
		{
1327
			$names[] = $address['name'];
1328
			$emails[] =$address['email'];
1329
		}
1330
1331
		$type = isset($this->enums['type']['email']) ? 'email' : 'note';
1332
		$status = isset($this->status['defaults'][$type]) ? $this->status['defaults'][$type] : 'done';
1333
		$info = array(
1334
			'info_id' => 0,
1335
			'info_type' => $type,
1336
			'info_from' => implode(', ',$names) . implode(', ', $emails),
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $emails seems to be defined by a foreach iteration on line 1325. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
Comprehensibility Best Practice introduced by
The variable $names seems to be defined by a foreach iteration on line 1325. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
1337
			'info_subject' => $_subject,
1338
			'info_des' => $_message,
1339
			'info_startdate' => Api\DateTime::server2user($_date),
1340
			'info_status' => $status,
1341
			'info_priority' => 1,
1342
			'info_percent' => $status == 'done' ? 100 : 0,
1343
			'referer' => false,
1344
			'link_to' => array(
1345
				'to_app' => 'infolog',
1346
				'to_id' => 0,
1347
			),
1348
		);
1349
		if ($GLOBALS['egw_info']['user']['preferences']['infolog']['cat_add_default']) $info['info_cat'] = $GLOBALS['egw_info']['user']['preferences']['infolog']['cat_add_default'];
1350
		// find the addressbookentry to link with
1351
		$addressbook = new Api\Contacts();
1352
		$contacts = array();
1353
		foreach ($emails as $mailadr)
1354
		{
1355
			$contacts = array_merge($contacts,(array)$addressbook->search(
1356
				array(
1357
					'email' => $mailadr,
1358
					'email_home' => $mailadr
1359
				),True,'','','',false,'OR',false,null,'',false));
1360
		}
1361
		if (!$contacts || !is_array($contacts) || !is_array($contacts[0]))
1362
		{
1363
			$info['msg'] = lang('Attention: No Contact with address %1 found.',$info['info_from']);
0 ignored issues
show
Unused Code introduced by
The call to lang() has too many arguments starting with $info['info_from']. ( Ignorable by Annotation )

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

1363
			$info['msg'] = /** @scrutinizer ignore-call */ lang('Attention: No Contact with address %1 found.',$info['info_from']);

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...
1364
			$info['info_custom_from'] = true;	// show the info_from line and NOT only the link
1365
		}
1366
		else
1367
		{
1368
			// create the first address as info_contact
1369
			$contact = array_shift($contacts);
1370
			$info['info_contact'] = 'addressbook:'.$contact['id'];
1371
			// create the rest a "ordinary" links
1372
			foreach ($contacts as $contact)
1373
			{
1374
				Link::link('infolog',$info['link_to']['to_id'],'addressbook',$contact['id']);
1375
			}
1376
		}
1377
		if (is_array($_attachments))
0 ignored issues
show
introduced by
The condition is_array($_attachments) is always true.
Loading history...
1378
		{
1379
			foreach ($_attachments as $attachment)
1380
			{
1381
				if($attachment['egw_data'])
1382
				{
1383
					Link::link('infolog',$info['link_to']['to_id'],Link::DATA_APPNAME,  $attachment);
1384
				}
1385
				else if(is_readable($attachment['tmp_name']) ||
1386
					(Vfs::is_readable($attachment['tmp_name']) && parse_url($attachment['tmp_name'], PHP_URL_SCHEME) === 'vfs'))
1387
				{
1388
					Link::link('infolog',$info['link_to']['to_id'],'file',  $attachment);
1389
				}
1390
			}
1391
		}
1392
		return $info;
1393
	}
1394
1395
	/**
1396
	 * get title for an infolog entry identified by $info
1397
	 *
1398
	 * Is called as hook to participate in the linking
1399
	 *
1400
	 * @param int|array $info int info_id or array with infolog entry
1401
	 * @return string|boolean string with the title, null if $info not found, false if no perms to view
1402
	 */
1403
	function link_title($info)
1404
	{
1405
		if (!is_array($info))
1406
		{
1407
			$info = $this->read( $info,false );
1408
		}
1409
		if (!$info)
1410
		{
1411
			return $info;
1412
		}
1413
		$title = !empty($info['info_subject']) ? $info['info_subject'] :self::subject_from_des($info['info_descr']);
1414
		return $title.($GLOBALS['egw_info']['user']['preferences']['infolog']['show_id']?' (#'.$info['info_id'].')':'');
1415
	}
1416
1417
	/**
1418
	 * Return multiple titles fetched by a single query
1419
	 *
1420
	 * @param array $ids
1421
	 */
1422
	function link_titles(array $ids)
1423
	{
1424
		$titles = array();
1425
		foreach ($this->search($params=array(
0 ignored issues
show
Bug introduced by
$params = array('col_fil...ray('info_id' => $ids)) cannot be passed to infolog_bo::search() as the parameter $query expects a reference. ( Ignorable by Annotation )

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

1425
		foreach ($this->search(/** @scrutinizer ignore-type */ $params=array(
Loading history...
1426
			'col_filter' => array('info_id' => $ids),
1427
		)) as $info)
1428
		{
1429
			$titles[$info['info_id']] = $this->link_title($info);
1430
		}
1431
		foreach (array_diff($ids,array_keys($titles)) as $id)
1432
		{
1433
			$titles[$id] = false;	// we assume every not returned entry to be not readable, as we notify the link class about all deletes
1434
		}
1435
		return $titles;
1436
	}
1437
1438
	/**
1439
	 * query infolog for entries matching $pattern
1440
	 *
1441
	 * Is called as hook to participate in the linking
1442
	 *
1443
	 * @param string $pattern pattern to search
1444
	 * @param array $options Array of options for the search
1445
	 * @return array with info_id - title pairs of the matching entries
1446
	 */
1447
	function link_query($pattern, Array &$options = array())
1448
	{
1449
		$query = array(
1450
			'search' => $pattern,
1451
			'start'  => $options['start'],
1452
			'num_rows'	=>	$options['num_rows'],
1453
			'subs'   => true,
1454
		);
1455
		$ids = $this->search($query);
1456
		$options['total'] = $query['total'];
1457
		$content = array();
1458
		if (is_array($ids))
0 ignored issues
show
introduced by
The condition is_array($ids) is always true.
Loading history...
1459
		{
1460
			foreach(array_keys($ids) as $id)
1461
			{
1462
				$content[$id] = $this->link_title($id);
1463
			}
1464
		}
1465
		return $content;
1466
	}
1467
1468
	/**
1469
	 * Check access to the file store
1470
	 *
1471
	 * @param int|array $id id of entry or entry array
1472
	 * @param int $check Acl::READ for read and Acl::EDIT for write or delete access
1473
	 * @param string $rel_path = null currently not used in InfoLog
1474
	 * @param int $user = null for which user to check, default current user
1475
	 * @return boolean true if access is granted or false otherwise
1476
	 */
1477
	function file_access($id,$check,$rel_path=null,$user=null)
1478
	{
1479
		unset($rel_path);	// not used
1480
		return $this->check_access($id,$check,0,$user);
1481
	}
1482
1483
	/**
1484
	 * Set the cache of the link class (title, file_access) for the given infolog entry
1485
	 *
1486
	 * @param array $info
1487
	 */
1488
	function set_link_cache(array $info)
1489
	{
1490
		Link::set_cache('infolog',$info['info_id'],
1491
			$this->link_title($info),
1492
			$this->file_access($info,Acl::EDIT) ? EGW_ACL_READ|EGW_ACL_EDIT :
1493
			($this->file_access($info,Acl::READ) ? Acl::READ : 0));
1494
	}
1495
1496
	/**
1497
	 * hook called be calendar to include events or todos in the cal-dayview
1498
	 *
1499
	 * @param int $args[year], $args[month], $args[day] date of the events
1500
	 * @param int $args[owner] owner of the events
1501
	 * @param string $args[location] calendar_include_{events|todos}
1502
	 * @return array of events (array with keys starttime, endtime, title, view, icon, content)
1503
	 */
1504
	function cal_to_include($args)
1505
	{
1506
		//echo "<p>cal_to_include("; print_r($args); echo ")</p>\n";
1507
		$user = (int) $args['owner'];
1508
		if ($user <= 0 && !checkdate($args['month'],$args['day'],$args['year']))
1509
		{
1510
			return False;
1511
		}
1512
		Api\Translation::add_app('infolog');
1513
1514
		$do_events = $args['location'] == 'calendar_include_events';
1515
		$to_include = array();
1516
		$date_wanted = sprintf('%04d/%02d/%02d',$args['year'],$args['month'],$args['day']);
1517
		$query = array(
1518
			'order' => $args['order'] ? $args['order'] : 'info_startdate',
1519
			'sort'  => $args['sort'] ? $args['sort'] : ($do_events ? 'ASC' : 'DESC'),
1520
			'filter'=> "user$user".($do_events ? 'date' : 'opentoday').$date_wanted,
1521
			'start' => 0,
1522
		);
1523
		if ($GLOBALS['egw_info']['user']['preferences']['infolog']['cal_show'] || $GLOBALS['egw_info']['user']['preferences']['infolog']['cal_show'] === '0')
1524
		{
1525
			$query['col_filter']['info_type'] = explode(',',$GLOBALS['egw_info']['user']['preferences']['infolog']['cal_show']);
1526
		}
1527
		elseif ($this->customfields && !$GLOBALS['egw_info']['user']['preferences']['infolog']['cal_show_custom'])
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->customfields 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...
1528
		{
1529
			$query['col_filter']['info_type'] = array('task','phone','note','email');
1530
		}
1531
		while ($infos = $this->search($query))
1532
		{
1533
			foreach ($infos as $info)
1534
			{
1535
				$start = new Api\DateTime($info['info_startdate'],Api\DateTime::$user_timezone);
1536
				$title = ($do_events ? $start->format(false).' ' : '').
1537
					$info['info_subject'];
1538
				$view = Link::view('infolog',$info['info_id']);
1539
				$size = null;
1540
				$edit = Link::edit('infolog',$info['info_id'], $size);
1541
				$edit['size'] = $size;
1542
				$content=array();
1543
				$status = $this->status[$info['info_type']][$info['info_status']];
1544
				$icons = array();
1545
				foreach(array(
1546
					$info['info_type'] => 'navbar',
1547
					$status => 'status'
1548
				) as $icon => $default)
1549
				{
1550
					$icons[Api\Image::find('infolog',$icon) ? $icon : $default] = $icon;
1551
				}
1552
				$content[] = Api\Html::a_href($title,$view);
1553
				$html = Api\Html::table(array(1 => $content));
1554
1555
				$to_include[] = array(
1556
					'starttime' => $info['info_startdate'],
1557
					'endtime'   => ($info['info_enddate'] ? $info['info_enddate'] : $info['info_startdate']),
1558
					'title'     => $title,
1559
					'view'      => $view,
1560
					'edit'      => $edit,
1561
					'icons'     => $icons,
1562
					'content'   => $html,
1563
				);
1564
			}
1565
			if ($query['total'] <= ($query['start']+=count($infos)))
1566
			{
1567
				break;	// no more availible
1568
			}
1569
		}
1570
		//echo "boinfolog::cal_to_include("; print_r($args); echo ")<pre>"; print_r($to_include); echo "</pre>\n";
1571
		return $to_include;
1572
	}
1573
1574
	/**
1575
	 * Returm InfoLog (custom) information for projectmanager: status icon, type icon, css class
1576
	 *
1577
	 * @param array $args array with id's in $args['infolog']
1578
	 * @return array with id => array with values for keys 'status', 'icon', 'class'
1579
	 */
1580
	function pm_icons($args)
1581
	{
1582
		if (isset($args['infolog']) && count($args['infolog']))
1583
		{
1584
			$query = array(
1585
				'col_filter' => array('info_id' => $args['infolog']),
1586
				'subs' => true,
1587
				'cols' => 'main.info_id,info_type,info_status,info_percent,info_id_parent',
1588
			);
1589
			$infos = array();
1590
			foreach($this->search($query) as $row)
1591
			{
1592
				$infos[$row['info_id']] = array(
1593
					'status' => $row['info_type'] != 'phone' && $row['info_status'] == 'ongoing' ?
1594
						$row['info_percent'].'%' : 'infolog/'.$this->status[$row['info_type']][$row['info_status']],
1595
					'status_icon' => $row['info_type'] != 'phone' && $row['info_status'] == 'ongoing' ?
1596
						'ongoing' : 'infolog/'.$row['info_status'],
1597
					'class'  => $row['info_id_parent'] ? 'infolog_rowHasParent' : null,
1598
				);
1599
				if (Api\Image::find('infolog', $icon=$row['info_type'].'_element') ||
1600
					Api\Image::find('infolog', $icon=$row['info_type']))
1601
				{
1602
					$infos[$row['info_id']]['icon'] = 'infolog/'.$icon;
1603
				}
1604
			}
1605
			$anzSubs = $this->anzSubs(array_keys($infos));
1606
			if($anzSubs && is_array($anzSubs))
1607
			{
1608
				foreach($anzSubs as $info_id => $subs)
1609
				{
1610
					if ($subs) $infos[$info_id]['class'] .= ' infolog_rowHasSubs';
1611
				}
1612
			}
1613
		}
1614
		return $infos;
1615
	}
1616
1617
	var $categories;
1618
1619
	/**
1620
	 * Find existing categories in database by name or add categories that do not exist yet
1621
	 * currently used for ical/sif import
1622
	 *
1623
	 * @param array $catname_list names of the categories which should be found or added
1624
	 * @param int $info_id = -1 match against existing infolog and expand the returned category ids
1625
	 *  by the ones the user normally does not see due to category permissions - used to preserve categories
1626
	 * @return array category ids (found, added and preserved categories)
1627
	 */
1628
	function find_or_add_categories($catname_list, $info_id=-1)
1629
	{
1630
		if (!is_object($this->categories))
1631
		{
1632
			$this->categories = new Api\Categories($this->user,'infolog');
1633
		}
1634
		$old_cats_preserve = array();
1635
		if ($info_id && $info_id > 0)
1636
		{
1637
			// preserve Api\Categories without users read access
1638
			$old_infolog = $this->read($info_id);
1639
			$old_categories = explode(',',$old_infolog['info_cat']);
1640
			if (is_array($old_categories) && count($old_categories) > 0)
1641
			{
1642
				foreach ($old_categories as $cat_id)
1643
				{
1644
					if ($cat_id && !$this->categories->check_perms(Acl::READ, $cat_id))
1645
					{
1646
						$old_cats_preserve[] = $cat_id;
1647
					}
1648
				}
1649
			}
1650
		}
1651
1652
		$cat_id_list = array();
1653
		foreach ((array)$catname_list as $cat_name)
1654
		{
1655
			$cat_name = trim($cat_name);
1656
			$cat_id = $this->categories->name2id($cat_name, 'X-');
1657
1658
			if (!$cat_id)
1659
			{
1660
				// some SyncML clients (mostly phones) add an X- to the category names
1661
				if (strncmp($cat_name, 'X-', 2) == 0)
1662
				{
1663
					$cat_name = substr($cat_name, 2);
1664
				}
1665
				$cat_id = $this->categories->add(array('name' => $cat_name, 'descr' => $cat_name, 'access' => 'private'));
1666
			}
1667
1668
			if ($cat_id)
1669
			{
1670
				$cat_id_list[] = $cat_id;
1671
			}
1672
		}
1673
1674
		if (count($old_cats_preserve) > 0)
1675
		{
1676
			$cat_id_list = array_merge($old_cats_preserve, $cat_id_list);
1677
		}
1678
1679
		if (count($cat_id_list) > 1)
1680
		{
1681
			$cat_id_list = array_unique($cat_id_list);
1682
			// disable sorting until infolog supports multiple categories
1683
			// to make sure that the preserved category takes precedence over a new one from the client
1684
			/* sort($cat_id_list, SORT_NUMERIC); */
1685
		}
1686
1687
		return $cat_id_list;
1688
	}
1689
1690
	/**
1691
	 * Get names for categories specified by their id's
1692
	 *
1693
	 * @param array|string $cat_id_list array or comma-sparated list of id's
1694
	 * @return array with names
1695
	 */
1696
	function get_categories($cat_id_list)
1697
	{
1698
		if (!is_object($this->categories))
1699
		{
1700
			$this->categories = new Api\Categories($this->user,'infolog');
1701
		}
1702
1703
		if (!is_array($cat_id_list))
1704
		{
1705
			$cat_id_list = explode(',',$cat_id_list);
1706
		}
1707
		$cat_list = array();
1708
		foreach($cat_id_list as $cat_id)
1709
		{
1710
			if ($cat_id && $this->categories->check_perms(Acl::READ, $cat_id) &&
1711
					($cat_name = $this->categories->id2name($cat_id)) && $cat_name != '--')
1712
			{
1713
				$cat_list[] = $cat_name;
1714
			}
1715
		}
1716
1717
		return $cat_list;
1718
	}
1719
1720
	/**
1721
	 * Send all async infolog notification
1722
	 *
1723
	 * Called via the async service job 'infolog-async-notification'
1724
	 */
1725
	function async_notification()
1726
	{
1727
		if (!($users = $this->so->users_with_open_entries()))
1728
		{
1729
			return;
1730
		}
1731
		//error_log(__METHOD__."() users with open entries: ".implode(', ',$users));
1732
1733
		$save_account_id = $GLOBALS['egw_info']['user']['account_id'];
1734
		$save_prefs      = $GLOBALS['egw_info']['user']['preferences'];
1735
		foreach($users as $user)
1736
		{
1737
			if (!($email = $GLOBALS['egw']->accounts->id2name($user,'account_email'))) continue;
1738
			// create the enviroment for $user
1739
			$this->user = $GLOBALS['egw_info']['user']['account_id'] = $user;
1740
			$GLOBALS['egw']->preferences->__construct($user);
1741
			$GLOBALS['egw_info']['user']['preferences'] = $GLOBALS['egw']->preferences->read_repository();
1742
			$GLOBALS['egw']->acl->__construct($user);
1743
			$this->grants = $GLOBALS['egw']->acl->get_grants('infolog',$this->group_owners ? $this->group_owners : true);
0 ignored issues
show
Bug Best Practice introduced by
The property grants does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
1744
			$this->so = new infolog_so($this->grants);	// so caches it's filters
1745
1746
			$notified_info_ids = array();
1747
			foreach(array(
1748
				'notify_due_responsible'   => 'open-responsible-enddate',
1749
				'notify_due_delegated'     => 'open-delegated-enddate',
1750
				'notify_start_responsible' => 'open-responsible-date',
1751
				'notify_start_delegated'   => 'open-delegated-date',
1752
			) as $pref => $filter)
1753
			{
1754
				if (!($pref_value = $GLOBALS['egw_info']['user']['preferences']['infolog'][$pref])) continue;
1755
1756
				$filter .= date('Y-m-d',time()+24*60*60*(int)$pref_value);
1757
				//error_log(__METHOD__."() checking with filter '$filter' ($pref_value) for user $user ($email)");
1758
1759
				$params = array('filter' => $filter, 'custom_fields' => true, 'subs' => true);
1760
				foreach($this->so->search($params) as $info)
1761
				{
1762
					// check if we already send a notification for that infolog entry, eg. starting and due on same day
1763
					if (in_array($info['info_id'],$notified_info_ids)) continue;
1764
1765
					if (is_null($this->tracking) || $this->tracking->user != $user)
1766
					{
1767
						$this->tracking = new infolog_tracking($this);
1768
					}
1769
					switch($pref)
1770
					{
1771
						case 'notify_due_responsible':
1772
							$info['prefix'] = lang('Due %1',$this->enums['type'][$info['info_type']]);
0 ignored issues
show
Unused Code introduced by
The call to lang() has too many arguments starting with $this->enums['type'][$info['info_type']]. ( Ignorable by Annotation )

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

1772
							$info['prefix'] = /** @scrutinizer ignore-call */ lang('Due %1',$this->enums['type'][$info['info_type']]);

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...
1773
							$info['message'] = lang('%1 you are responsible for is due at %2',$this->enums['type'][$info['info_type']],
1774
								$this->tracking->datetime($info['info_enddate'],false));
1775
							break;
1776
						case 'notify_due_delegated':
1777
							$info['prefix'] = lang('Due %1',$this->enums['type'][$info['info_type']]);
1778
							$info['message'] = lang('%1 you delegated is due at %2',$this->enums['type'][$info['info_type']],
1779
								$this->tracking->datetime($info['info_enddate'],false));
1780
							break;
1781
						case 'notify_start_responsible':
1782
							$info['prefix'] = lang('Starting %1',$this->enums['type'][$info['info_type']]);
1783
							$info['message'] = lang('%1 you are responsible for is starting at %2',$this->enums['type'][$info['info_type']],
1784
								$this->tracking->datetime($info['info_startdate'],null));
1785
							break;
1786
						case 'notify_start_delegated':
1787
							$info['prefix'] = lang('Starting %1',$this->enums['type'][$info['info_type']]);
1788
							$info['message'] = lang('%1 you delegated is starting at %2',$this->enums['type'][$info['info_type']],
1789
								$this->tracking->datetime($info['info_startdate'],null));
1790
							break;
1791
					}
1792
					//error_log("notifiying $user($email) about $info[info_subject]: $info[message]");
1793
					$this->tracking->send_notification($info,null,$email,$user,$pref);
1794
1795
					$notified_info_ids[] = $info['info_id'];
1796
				}
1797
			}
1798
		}
1799
1800
		$GLOBALS['egw_info']['user']['account_id']  = $save_account_id;
1801
		$GLOBALS['egw_info']['user']['preferences'] = $save_prefs;
1802
	}
1803
1804
	/** conversion of infolog status to vtodo status
1805
	 * @private
1806
	 * @var array
1807
	 */
1808
	var $_status2vtodo = array(
1809
		'offer'       => 'NEEDS-ACTION',
1810
		'not-started' => 'NEEDS-ACTION',
1811
		'ongoing'     => 'IN-PROCESS',
1812
		'done'        => 'COMPLETED',
1813
		'cancelled'   => 'CANCELLED',
1814
		'billed'      => 'COMPLETED',
1815
		'template'    => 'CANCELLED',
1816
		'nonactive'   => 'CANCELLED',
1817
		'archive'     => 'CANCELLED',
1818
		'deferred'    => 'NEEDS-ACTION',
1819
		'waiting'     => 'IN-PROCESS',
1820
	);
1821
1822
	/** conversion of vtodo status to infolog status
1823
	 * @private
1824
	 * @var array
1825
	 */
1826
	var $_vtodo2status = array(
1827
		'NEEDS-ACTION' => 'not-started',
1828
		'NEEDS ACTION' => 'not-started',
1829
		'IN-PROCESS'   => 'ongoing',
1830
		'IN PROCESS'   => 'ongoing',
1831
		'COMPLETED'    => 'done',
1832
		'CANCELLED'    => 'cancelled',
1833
	);
1834
1835
	/**
1836
	 * Converts an infolog status into a vtodo status
1837
	 *
1838
	 * @param string $status see $this->status
1839
	 * @return string {CANCELLED|NEEDS-ACTION|COMPLETED|IN-PROCESS}
1840
	 */
1841
	function status2vtodo($status)
1842
	{
1843
		return isset($this->_status2vtodo[$status]) ? $this->_status2vtodo[$status] : 'NEEDS-ACTION';
1844
	}
1845
1846
	/**
1847
	 * Converts a vtodo status into an infolog status using the optional X-INFOLOG-STATUS
1848
	 *
1849
	 * X-INFOLOG-STATUS is only used, if translated to the vtodo-status gives the identical vtodo status
1850
	 * --> the user did not changed it
1851
	 *
1852
	 * @param string $_vtodo_status {CANCELLED|NEEDS-ACTION|COMPLETED|IN-PROCESS}
1853
	 * @param string $x_infolog_status preserved original infolog status
1854
	 * @return string
1855
	 */
1856
	function vtodo2status($_vtodo_status,$x_infolog_status=null)
1857
	{
1858
		$vtodo_status = strtoupper($_vtodo_status);
1859
1860
		if ($x_infolog_status && $this->status2vtodo($x_infolog_status) == $vtodo_status)
1861
		{
1862
			$status = $x_infolog_status;
1863
		}
1864
		else
1865
		{
1866
			$status = isset($this->_vtodo2status[$vtodo_status]) ? $this->_vtodo2status[$vtodo_status] : 'not-started';
1867
		}
1868
		return $status;
1869
	}
1870
1871
	/**
1872
	 * Get status of a single or all types
1873
	 *
1874
	 * As status value can have different translations depending on type, we list all translations
1875
	 *
1876
	 * @param string $type = null
1877
	 * @param array &$icons = null on return name of icons
1878
	 * @return array value => (commaseparated) translations
1879
	 */
1880
	function get_status($type=null, array &$icons=null)
1881
	{
1882
		// if filtered by type, show only the stati of the filtered type
1883
		if ($type && isset($this->status[$type]))
1884
		{
1885
			$statis = $icons = $this->status[$type];
1886
		}
1887
		else	// show all stati
1888
		{
1889
			$statis = $icons = array();
1890
			foreach($this->status as $t => $stati)
1891
			{
1892
				if ($t === 'defaults') continue;
1893
				foreach($stati as $val => $label)
1894
				{
1895
					$statis[$val][$label] = lang($label);
1896
					if (!isset($icons[$val])) $icons[$val] = $label;
1897
				}
1898
			}
1899
			foreach($statis as $val => &$labels)
1900
			{
1901
				$labels = implode(', ', $labels);
1902
			}
1903
		}
1904
		return $statis;
1905
	}
1906
1907
	/**
1908
	 * Activates an InfoLog entry (setting it's status from template or inactive depending on the completed percentage)
1909
	 *
1910
	 * @param array $info
1911
	 * @return string new status
1912
	 */
1913
	function activate($info)
1914
	{
1915
		switch((int)$info['info_percent'])
1916
		{
1917
			case 0:		return 'not-started';
1918
			case 100:	return 'done';
1919
		}
1920
		return 'ongoing';
1921
	}
1922
1923
	/**
1924
	 * Get the Parent ID of an InfoLog entry
1925
	 *
1926
	 * @param string $_guid
1927
	 * @return string parentID
1928
	 */
1929
	function getParentID($_guid)
1930
	{
1931
		#Horde::logMessage("getParentID($_guid)",  __FILE__, __LINE__, PEAR_LOG_DEBUG);
1932
1933
		$parentID = False;
1934
		$myfilter = array('col_filter' => array('info_uid'=>$_guid)) ;
1935
		if ($_guid && ($found=$this->search($myfilter)) && ($uidmatch = array_shift($found)))
1936
		{
1937
			$parentID = $uidmatch['info_id'];
1938
		}
1939
		return $parentID;
1940
	}
1941
1942
	/**
1943
	 * Try to find a matching db entry
1944
	 * This expects timestamps to be in server-time.
1945
	 *
1946
	 * @param array $infoData   the infolog data we try to find
1947
	 * @param boolean $relax = false if asked to relax, we only match against some key fields
1948
	 * @param string $tzid = null timezone, null => user time
1949
	 *
1950
	 * @return array of infolog_ids of matching entries
1951
	 */
1952
	function findInfo($infoData, $relax=false, $tzid=null)
1953
	{
1954
		$foundInfoLogs = array();
1955
		$filter = array();
1956
1957
		if ($this->log)
1958
		{
1959
			error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
1960
				. '('. ($relax ? 'RELAX, ': 'EXACT, ') . $tzid . ')[InfoData]:'
1961
				. array2string($infoData));
1962
		}
1963
1964
		if ($infoData['info_id']
1965
			&& ($egwData = $this->read($infoData['info_id'], true, 'server')))
1966
		{
1967
			// we only do a simple consistency check
1968
			if (!$relax || strpos($egwData['info_subject'], $infoData['info_subject']) === 0)
1969
			{
1970
				return array($egwData['info_id']);
1971
			}
1972
			if (!$relax) return array();
0 ignored issues
show
introduced by
The condition $relax is always true.
Loading history...
1973
		}
1974
		unset($infoData['info_id']);
1975
1976
		if (!$relax && !empty($infoData['info_uid']))
1977
		{
1978
			$filter = array('col_filter' => array('info_uid' => $infoData['info_uid']));
1979
			foreach($this->so->search($filter) as $egwData)
1980
			{
1981
				if (!$this->check_access($egwData,Acl::READ)) continue;
1982
				$foundInfoLogs[$egwData['info_id']] = $egwData['info_id'];
1983
			}
1984
			return $foundInfoLogs;
1985
		}
1986
		unset($infoData['info_uid']);
1987
1988
		if (empty($infoData['info_des']))
1989
		{
1990
			$description = false;
1991
		}
1992
		else
1993
		{
1994
			// ignore meta information appendices
1995
			$description = trim(preg_replace('/\s*\[[A-Z_]+:.*\].*/im', '', $infoData['info_des']));
1996
			$text = trim(preg_replace('/\s*\[[A-Z_]+:.*\]/im', '', $infoData['info_des']));
1997
			if ($this->log)
1998
			{
1999
				error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
2000
					. "()[description]: $description");
2001
			}
2002
			// Avoid quotation problems
2003
			$matches = null;
2004
			if (preg_match_all('/[\x20-\x7F]*/m', $text, $matches, PREG_SET_ORDER))
2005
			{
2006
				$text = '';
2007
				foreach ($matches as $chunk)
2008
				{
2009
					if (strlen($text) <  strlen($chunk[0]))
2010
					{
2011
						$text = $chunk[0];
2012
					}
2013
				}
2014
				if ($this->log)
2015
				{
2016
					error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
2017
						. "()[search]: $text");
2018
				}
2019
				$filter['search'] = $text;
2020
			}
2021
		}
2022
		$this->time2time($infoData, $tzid, false);
2023
2024
		$filter['col_filter'] = $infoData;
2025
		// priority does not need to match
2026
		unset($filter['col_filter']['info_priority']);
2027
		// we ignore description and location first
2028
		unset($filter['col_filter']['info_des']);
2029
		unset($filter['col_filter']['info_location']);
2030
2031
		foreach ($this->so->search($filter) as $itemID => $egwData)
2032
		{
2033
			if (!$this->check_access($egwData,Acl::READ)) continue;
2034
2035
			switch ($infoData['info_type'])
2036
			{
2037
				case 'task':
2038
					if (!empty($egwData['info_location']))
2039
					{
2040
						$egwData['info_location'] = str_replace("\r\n", "\n", $egwData['info_location']);
2041
					}
2042
					if (!$relax &&
2043
					!empty($infoData['info_location']) && (empty($egwData['info_location'])
2044
						|| strpos($egwData['info_location'], $infoData['info_location']) !== 0))
2045
					{
2046
						if ($this->log)
2047
						{
2048
							error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
2049
								. '()[location mismatch]: '
2050
								. $infoData['info_location'] . ' <> ' . $egwData['info_location']);
2051
						}
2052
						continue 2;	// +1 for switch
2053
					}
2054
				default:
2055
					if (!empty($egwData['info_des']))
2056
					{
2057
						$egwData['info_des'] = str_replace("\r\n", "\n", $egwData['info_des']);
2058
					}
2059
					if (!$relax && ($description && empty($egwData['info_des'])
2060
						|| !empty($egwData['info_des']) && empty($infoData['info_des'])
2061
						|| strpos($egwData['info_des'], $description) === false))
2062
					{
2063
						if ($this->log)
2064
						{
2065
							error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
2066
								. '()[description mismatch]: '
2067
								. $infoData['info_des'] . ' <> ' . $egwData['info_des']);
2068
						}
2069
						continue 2;	// +1 for switch
2070
					}
2071
					// no further criteria to match
2072
					$foundInfoLogs[$egwData['info_id']] = $egwData['info_id'];
2073
			}
2074
		}
2075
2076
		if (!$relax && !empty($foundInfoLogs))
2077
		{
2078
			if ($this->log)
2079
			{
2080
				error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
2081
					. '()[FOUND]:' . array2string($foundInfoLogs));
2082
			}
2083
			return $foundInfoLogs;
2084
		}
2085
2086
		if ($relax)
2087
		{
2088
			unset($filter['search']);
2089
		}
2090
2091
		// search for matches by date only
2092
		unset($filter['col_filter']['info_startdate']);
2093
		unset($filter['col_filter']['info_enddate']);
2094
		unset($filter['col_filter']['info_datecompleted']);
2095
		// Some devices support lesser stati
2096
		unset($filter['col_filter']['info_status']);
2097
2098
		// try tasks without category
2099
		unset($filter['col_filter']['info_cat']);
2100
2101
		// Horde::logMessage("findVTODO Filter\n"
2102
		//	. print_r($filter, true),
2103
		//	__FILE__, __LINE__, PEAR_LOG_DEBUG);
2104
		foreach ($this->so->search($filter) as $itemID => $egwData)
2105
		{
2106
			if (!$this->check_access($egwData,Acl::READ)) continue;
2107
			// Horde::logMessage("findVTODO Trying\n"
2108
			//	. print_r($egwData, true),
2109
			//	__FILE__, __LINE__, PEAR_LOG_DEBUG);
2110
			if (isset($infoData['info_cat'])
2111
					&& isset($egwData['info_cat']) && $egwData['info_cat']
2112
															   && $infoData['info_cat'] != $egwData['info_cat'])
2113
			{
2114
				if ($this->log)
2115
				{
2116
					error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
2117
						. '()[category mismatch]: '
2118
						. $infoData['info_cat'] . ' <> ' . $egwData['info_cat']);
2119
				}
2120
				continue;
2121
			}
2122
			if (isset($infoData['info_startdate']) && $infoData['info_startdate'])
2123
			{
2124
				// We got a startdate from client
2125
				if (isset($egwData['info_startdate']) && $egwData['info_startdate'])
2126
				{
2127
					// We compare the date only
2128
					$taskTime = new Api\DateTime($infoData['info_startdate'],Api\DateTime::$server_timezone);
2129
					$egwTime = new Api\DateTime($egwData['info_startdate'],Api\DateTime::$server_timezone);
2130
					if ($taskTime->format('Ymd') != $egwTime->format('Ymd'))
2131
					{
2132
						if ($this->log)
2133
						{
2134
							error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
2135
								. '()[start mismatch]: '
2136
								. $taskTime->format('Ymd') . ' <> ' . $egwTime->format('Ymd'));
2137
						}
2138
						continue;
2139
					}
2140
				}
2141
				elseif (!$relax)
2142
				{
2143
					if ($this->log)
2144
					{
2145
						error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
2146
							. '()[start mismatch]');
2147
					}
2148
					continue;
2149
				}
2150
			}
2151
			if ($infoData['info_type'] == 'task')
2152
			{
2153
				if (isset($infoData['info_status']) && isset($egwData['info_status'])
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: (IssetNode && IssetNode ...info_status'] == 'done', Probably Intended Meaning: IssetNode && IssetNode &...nfo_status'] == 'done')
Loading history...
2154
						&& $egwData['info_status'] == 'done'
2155
							&& $infoData['info_status'] != 'done' ||
2156
								$egwData['info_status'] != 'done'
2157
									&& $infoData['info_status'] == 'done')
2158
				{
2159
					if ($this->log)
2160
					{
2161
						error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
2162
							. '()[status mismatch]: '
2163
							. $infoData['info_status'] . ' <> ' . $egwData['info_status']);
2164
					}
2165
					continue;
2166
				}
2167
				if (isset($infoData['info_enddate']) && $infoData['info_enddate'])
2168
				{
2169
					// We got a enddate from client
2170
					if (isset($egwData['info_enddate']) && $egwData['info_enddate'])
2171
					{
2172
						// We compare the date only
2173
						$taskTime = new Api\DateTime($infoData['info_enddate'],Api\DateTime::$server_timezone);
2174
						$egwTime = new Api\DateTime($egwData['info_enddate'],Api\DateTime::$server_timezone);
2175
						if ($taskTime->format('Ymd') != $egwTime->format('Ymd'))
2176
						{
2177
							if ($this->log)
2178
							{
2179
								error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
2180
									. '()[DUE mismatch]: '
2181
									. $taskTime->format('Ymd') . ' <> ' . $egwTime->format('Ymd'));
2182
							}
2183
							continue;
2184
						}
2185
					}
2186
					elseif (!$relax)
2187
					{
2188
						if ($this->log)
2189
						{
2190
							error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
2191
								. '()[DUE mismatch]');
2192
						}
2193
						continue;
2194
					}
2195
				}
2196
				if (isset($infoData['info_datecompleted']) && $infoData['info_datecompleted'])
2197
				{
2198
					// We got a completed date from client
2199
					if (isset($egwData['info_datecompleted']) && $egwData['info_datecompleted'])
2200
					{
2201
						// We compare the date only
2202
						$taskTime = new Api\DateTime($infoData['info_datecompleted'],Api\DateTime::$server_timezone);
2203
						$egwTime = new Api\DateTime($egwData['info_datecompleted'],Api\DateTime::$server_timezone);
2204
						if ($taskTime->format('Ymd') != $egwTime->format('Ymd'))
2205
						{
2206
							if ($this->log)
2207
							{
2208
								error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
2209
									. '()[completed mismatch]: '
2210
									. $taskTime->format('Ymd') . ' <> ' . $egwTime->format('Ymd'));
2211
							}
2212
							continue;
2213
						}
2214
					}
2215
					elseif (!$relax)
2216
					{
2217
						if ($this->log)
2218
						{
2219
							error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
2220
								. '()[completed mismatch]');
2221
						}
2222
						continue;
2223
					}
2224
				}
2225
				elseif (!$relax && isset($egwData['info_datecompleted']) && $egwData['info_datecompleted'])
2226
				{
2227
					if ($this->log)
2228
					{
2229
						error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
2230
							. '()[completed mismatch]');
2231
					}
2232
					continue;
2233
				}
2234
			}
2235
			$foundInfoLogs[$itemID] = $itemID;
2236
		}
2237
		if ($this->log)
2238
		{
2239
			error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
2240
				. '()[FOUND]:' . array2string($foundInfoLogs));
2241
		}
2242
		return $foundInfoLogs;
2243
	}
2244
}
2245