Tracking::get_body()   F
last analyzed

Complexity

Conditions 24
Paths 1059

Size

Total Lines 52
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 24
eloc 25
nc 1059
nop 5
dl 0
loc 52
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

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:

1
<?php
2
/**
3
 * EGroupware API - abstract base class for tracking (history log, notifications, ...)
4
 *
5
 * @link http://www.egroupware.org
6
 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
7
 * @package api
8
 * @subpackage storage
9
 * @copyright (c) 2007-16 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
10
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
11
 * @version $Id$
12
 */
13
14
namespace EGroupware\Api\Storage;
15
16
use EGroupware\Api;
17
18
// explicitly reference classes still in phpgwapi or otherwise outside api
19
use notifications;
20
21
22
/**
23
 * Abstract base class for trackering:
24
 *  - logging all modifications of an entry
25
 *  - notifying users about changes in an entry
26
 *
27
 * You need to extend these class in your application:
28
 *	1. set the required class-vars: app, id_field
29
 *	2. optional set class-vars: creator_field, assigned_field, check2prefs
30
 *	3. implement the required methods: get_config, get_details
31
 *	4. optionally re-implement: get_title, get_subject, get_body, get_attachments, get_link, get_notification_link, get_message
32
 * They are all documented in this file via phpDocumentor comments.
33
 *
34
 * Translate field-name to history status field:
35
 * As history status was only char(2) prior to EGroupware 1.6, a mapping was necessary.
36
 * Now it's varchar(64) and a mapping makes no sense for new applications, just list
37
 * all fields to log as key AND value!
38
 *
39
 * History login supports now 1:N relations on a base record. To use that you need:
40
 * - to have the 1:N relation as array of arrays with the values of that releation, eg:
41
 * $data = array(
42
 * 	'id' => 123,
43
 *  'title' => 'Something',
44
 *  'date'  => '2009-08-21 14:42:00',
45
 * 	'participants' => array(
46
 * 		array('account_id' => 15, 'name' => 'User Hugo', 'status' => 'A', 'quantity' => 1),
47
 * 		array('account_id' => 17, 'name' => 'User Bert', 'status' => 'U', 'quantity' => 3),
48
 *  ),
49
 * );
50
 * - set field2history as follows
51
 * $field2history = array(
52
 * 	'id' => 'id',
53
 *  'title' => 'title',
54
 *  'participants' => array('uid','status','quantity'),
55
 * );
56
 * - set content for history log widget:
57
 * $content['history'] = array(
58
 * 	'id' => 123,
59
 *  'app' => 'calendar',
60
 *  'num_rows' => 50, // optional, defaults to 50
61
 *  'status-widgets' => array(
62
 * 		'title' => 'label',	// no need to set, as default is label
63
 * 		'date'  => 'datetime',
64
 * 		'participants' = array(
65
 * 			'select-account',
66
 * 			array('U' => 'Unknown', 'A' => 'Accepted', 'R' => 'Rejected'),
67
 * 			'integer',
68
 * 		),
69
 *  ),
70
 * );
71
 * - set lables for history:
72
 * $sel_options['status'] = array(
73
 * 	'title' => 'Title',
74
 *  'date'  => 'Starttime',
75
 *  'participants' => 'Participants: User, Status, Quantity',	// a single label!
76
 * );
77
 *
78
 * The above is also an example for using regular history login in EGroupware (by skipping the 'participants' key).
79
 */
80
abstract class Tracking
81
{
82
	/**
83
	 * Application we are tracking
84
	 *
85
	 * @var string
86
	 */
87
	var $app;
88
	/**
89
	 * Name of the id-field, used as id in the history log (required!)
90
	 *
91
	 * @var string
92
	 */
93
	var $id_field;
94
	/**
95
	 * Name of the field with the creator id, if the creator of an entry should be notified
96
	 *
97
	 * @var string
98
	 */
99
	var $creator_field;
100
	/**
101
	 * Name of the field with the id(s) of assinged users, if they should be notified
102
	 *
103
	 * @var string
104
	 */
105
	var $assigned_field;
106
	/**
107
	 * Can be used to map the following prefs to different names:
108
	 *  - notify_creator  - user wants to be notified for items he created
109
	 *  - notify_assigned - user wants to be notified for items assigned to him
110
	 * @var array
111
	 */
112
	var $check2pref;
113
	/**
114
	 * Translate field-name to history status field (see comment in class header)
115
	 *
116
	 * @var array
117
	 */
118
	var $field2history = array();
119
	/**
120
	 * Should the user (passed to the track method or current user if not passed) be used as sender or get_config('sender')
121
	 *
122
	 * @var boolean
123
	 */
124
	var $prefer_user_as_sender = true;
125
	/**
126
	 * Should the current user be email-notified (about change he made himself)
127
	 *
128
	 * Popup notifications are never send to the current user!
129
	 *
130
	 * @var boolean
131
	 */
132
	var $notify_current_user = false;
133
134
	/**
135
	 * Class to use for generating the notifications.
136
	 * Normally, just the notification class but for testing we pass in a mocked
137
	 * class
138
	 *
139
	 * @var notification_class
0 ignored issues
show
Bug introduced by
The type EGroupware\Api\Storage\notification_class was not found. Maybe you did not declare it correctly or list all dependencies?

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

filter:
    dependency_paths: ["lib/*"]

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

Loading history...
140
	 */
141
	protected $notification_class = notifications::class;
142
143
	/**
144
	 * Array with error-messages if track($data,$old) returns false
145
	 *
146
	 * @var array
147
	 */
148
	var $errors = array();
149
150
	/**
151
	 * Instance of the History object for the app we are tracking
152
	 *
153
	 * @var History
154
	 */
155
	protected $historylog;
156
157
	/**
158
	 * Current user, can be set via bo_tracking::track(,,$user)
159
	 *
160
	 * @access private
161
	 * @var int;
162
	 */
163
	var $user;
164
165
	/**
166
	 * Datetime format of the currently notified user (send_notificaton)
167
	 *
168
	 * @var string
169
	 */
170
	var $datetime_format;
171
	/**
172
	 * Should the class allow html content (for notifications)
173
	 *
174
	 * @var boolean
175
	 */
176
	var $html_content_allow = false;
177
178
	/**
179
	 * Custom fields of type link entry or application
180
	 *
181
	 * Used to automatic create or update a link
182
	 *
183
	 * @var array field => application name pairs (or empty for link entry)
184
	 */
185
	var $cf_link_fields = array();
186
187
	/**
188
	 * Separator for 1:N relations
189
	 *
190
	 */
191
	const ONE2N_SEPERATOR = '~|~';
192
193
	/**
194
	 * Marker for change stored as unified diff, not old/new value
195
	 * Diff is in the new value, marker in old value
196
	 */
197
	const DIFF_MARKER = '***diff***';
198
199
	/**
200
	 * Config name for custom notification message
201
	 */
202
	const CUSTOM_NOTIFICATION = 'custom_notification';
203
204
	/**
205
	 * Constructor
206
	 *
207
	 * @param string $cf_app = null if set, custom field names get added to $field2history
208
	 * @return bo_tracking
0 ignored issues
show
Bug introduced by
The type EGroupware\Api\Storage\bo_tracking was not found. Maybe you did not declare it correctly or list all dependencies?

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

filter:
    dependency_paths: ["lib/*"]

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

Loading history...
209
	 */
210
	function __construct($cf_app = null, $notification_class=false)
211
	{
212
		if ($cf_app)
213
		{
214
			$linkable_cf_types = array('link-entry')+array_keys(Api\Link::app_list());
215
			foreach(Customfields::get($cf_app, true) as $cf_name => $cf_data)
216
			{
217
				$this->field2history['#'.$cf_name] = '#'.$cf_name;
218
219
				if (in_array($cf_data['type'],$linkable_cf_types))
220
				{
221
					$this->cf_link_fields['#'.$cf_name] = $cf_data['type'] == 'link-entry' ? '' : $cf_data['type'];
222
				}
223
			}
224
		}
225
		if($notification_class)
226
		{
227
			$this->notification_class = $notification_class;
228
		}
229
	}
230
231
	/**
232
	 * Get the details of an entry
233
	 *
234
	 * You can/should call $this->get_customfields() to add custom fields.
235
	 *
236
	 * @param array|object $data
237
	 * @param int|string $receiver nummeric account_id or email address
238
	 * @return array of details as array with values for keys 'label','value','type'
239
	 */
240
	function get_details($data,$receiver=null)
241
	{
242
		unset($data, $receiver);	// not uses as just a stub
243
244
		return array();
245
	}
246
247
	/**
248
	 * Get custom fields of an entry of an entry
249
	 *
250
	 * @param array|object $data
251
	 * @param string $only_type2 = null if given only return fields of type2 == $only_type2
252
	 * @param int $user = false Use this user for custom field permissions, or false
253
	 *	to strip all private custom fields
254
	 *
255
	 * @return array of details as array with values for keys 'label','value','type'
256
	 */
257
	function get_customfields($data, $only_type2=null, $user = false)
258
	{
259
		$details = array();
260
		if(!is_numeric($user))
261
		{
262
			$user = false;
263
		}
264
265
		if (($cfs = Customfields::get($this->app, $user, $only_type2)))
266
		{
267
			$header_done = false;
268
			foreach($cfs as $name => $field)
269
			{
270
				if (in_array($field['type'], Customfields::$non_printable_fields)) continue;
271
272
				// Sometimes cached customfields let private fields the user can access
273
				// leak through.  If no specific user provided, make sure we don't expose them.
274
				if ($user === false && $field['private']) continue;
275
276
				if (!$header_done)
277
				{
278
					$details['custom'] = array(
279
						'value' => lang('Custom fields').':',
280
						'type'  => 'reply',
281
					);
282
					$header_done = true;
283
				}
284
				//error_log(__METHOD__."() $name: data['#$name']=".array2string($data['#'.$name]).", field[values]=".array2string($field['values']));
285
				$details['#'.$name] = array(
286
					'label' => $field['label'],
287
					'value' => Customfields::format($field, $data['#'.$name]),
288
				);
289
				//error_log("--> details['#$name']=".array2string($details['#'.$name]));
290
			}
291
		}
292
		return $details;
293
	}
294
295
	/**
296
	 * Get a config value, which can depend on $data and $old
297
	 *
298
	 * Need to be implemented in your extended tracking class!
299
	 *
300
	 * @param string $name possible values are:
301
	 *  - 'assigned' array of users to use instead of a field in the data
302
	 * 	- 'copy' array of email addresses notifications should be copied too, can depend on $data
303
	 *  - 'lang' string lang code for copy mail
304
	 *  - 'subject' string subject line for the notification of $data,$old, defaults to link-title
305
	 *  - 'link' string of link to view $data
306
	 *  - 'sender' sender of email
307
	 *  - 'reply_to' reply to of email
308
	 *  - 'skip_notify' array of email addresses that should _not_ be notified
309
	 *  - CUSTOM_NOTIFICATION string notification body message.  Merge print placeholders are allowed.
310
	 * @param array $data current entry
311
	 * @param array $old = null old/last state of the entry or null for a new entry
312
	 * @return mixed
313
	 */
314
	protected function get_config($name,$data,$old=null)
315
	{
316
		unset($name, $data, $old);	// not used as just a stub
317
318
		return null;
319
	}
320
321
	/**
322
	 * Tracks the changes in one entry $data, by comparing it with the last version in $old
323
	 *
324
	 * @param array $data current entry
325
	 * @param array $old = null old/last state of the entry or null for a new entry
326
	 * @param int $user = null user who made the changes, default to current user
327
	 * @param boolean $deleted = null can be set to true to let the tracking know the item got deleted or undeleted
328
	 * @param array $changed_fields = null changed fields from ealier call to $this->changed_fields($data,$old), to not compute it again
329
	 * @param boolean $skip_notification = false do NOT send any notification
330
	 * @return int|boolean false on error, integer number of changes logged or true for new entries ($old == null)
331
	 */
332
	public function track(array $data,array $old=null,$user=null,$deleted=null,array $changed_fields=null,$skip_notification=false)
333
	{
334
		$this->user = !is_null($user) ? $user : $GLOBALS['egw_info']['user']['account_id'];
335
336
		$changes = true;
337
		//error_log(__METHOD__.__LINE__);
338
		if ($old && $this->field2history)
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->field2history 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...
339
		{
340
			//error_log(__METHOD__.__LINE__.' Changedfields:'.print_r($changed_fields,true));
341
			$changes = $this->save_history($data,$old,$deleted,$changed_fields);
342
			//error_log(__METHOD__.__LINE__.' Changedfields:'.print_r($changed_fields,true));
343
			//error_log(__METHOD__.__LINE__.' Changes:'.print_r($changes,true));
344
		}
345
346
		//error_log(__METHOD__.__LINE__.' LinkFields:'.array2string($this->cf_link_fields));
347
		if ($changes && $this->cf_link_fields)
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->cf_link_fields 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...
348
		{
349
			$this->update_links($data,(array)$old);
350
		}
351
		// do not run do_notifications if we have no changes
352
		if ($changes && !$skip_notification && !$this->do_notifications($data,$old,$deleted))
353
		{
354
			$changes = false;
355
		}
356
		return $changes;
357
	}
358
359
	/**
360
	 * Store a link for each custom field linking to an other application and update them
361
	 *
362
	 * @param array $data
363
	 * @param array $old
364
	 */
365
	protected function update_links(array $data, array $old)
366
	{
367
		//error_log(__METHOD__.__LINE__.array2string($data).function_backtrace());
368
		//error_log(__METHOD__.__LINE__.array2string($this->cf_link_fields));
369
		foreach(array_keys((array)$this->cf_link_fields) as $name)
370
		{
371
			//error_log(__METHOD__.__LINE__.' Field:'.$name. ' Value (new):'.array2string($data[$name]));
372
			//error_log(__METHOD__.__LINE__.' Field:'.$name. ' Value (old):'.array2string($old[$name]));
373
			if (is_array($data[$name]) && array_key_exists('id',$data[$name])) $data[$name] = $data[$name]['id'];
374
			if (is_array($old[$name]) && array_key_exists('id',$old[$name])) $old[$name] = $old[$name]['id'];
375
			//error_log(__METHOD__.__LINE__.'(After processing) Field:'.$name. ' Value (new):'.array2string($data[$name]));
376
			//error_log(__METHOD__.__LINE__.'(After processing) Field:'.$name. ' Value (old):'.array2string($old[$name]));
377
		}
378
		$current_ids = array_unique(array_diff(array_intersect_key($data,$this->cf_link_fields),array('',0,NULL)));
379
		$old_ids = $old ? array_unique(array_diff(array_intersect_key($old,$this->cf_link_fields),array('',0,NULL))) : array();
380
		//error_log(__METHOD__.__LINE__.array2string($current_ids));
381
		//error_log(__METHOD__.__LINE__.array2string($old_ids));
382
		// create links for added application entry
383
		foreach(array_diff($current_ids,$old_ids) as $name => $id)
384
		{
385
			if (!($app = $this->cf_link_fields[$name]))
386
			{
387
				list($app,$id) = explode(':',$id);
388
				if (!$id) continue;	// can be eg. 'addressbook:', if no contact selected
389
			}
390
			$source_id = $data[$this->id_field];
391
			//error_log(__METHOD__.__LINE__.array2string($source_id));
392
			if ($source_id) Api\Link::link($this->app,$source_id,$app,$id);
393
			//error_log(__METHOD__.__LINE__."Api\Link::link('$this->app',".array2string($source_id).",'$app',$id);");
394
			//echo "<p>Api\Link::link('$this->app',{$data[$this->id_field]},'$app',$id);</p>\n";
395
		}
396
397
		// unlink removed application entries
398
		foreach(array_diff($old_ids,$current_ids) as $name => $id)
399
		{
400
			if (!isset($data[$name])) continue;	// ignore not set link cf's, eg. from sync clients
401
			if (!($app = $this->cf_link_fields[$name]))
402
			{
403
				list($app,$id) = explode(':',$id);
404
				if (!$id) continue;
405
			}
406
			$source_id = $data[$this->id_field];
407
			if ($source_id) Api\Link::unlink(null,$this->app,$source_id,0,$app,$id);
408
			//echo "<p>Api\Link::unlink(NULL,'$this->app',{$data[$this->id_field]},0,'$app',$id);</p>\n";
409
		}
410
	}
411
412
	/**
413
	 * Save changes to the history log
414
	 *
415
	 * @internal use only track($data,$old)
416
	 * @param array $data current entry
417
	 * @param array $old = null old/last state of the entry or null for a new entry
418
	 * @param boolean $deleted = null can be set to true to let the tracking know the item got deleted or undelted
419
	 * @param array $changed_fields = null changed fields from ealier call to $this->changed_fields($data,$old), to not compute it again
420
	 * @return int number of log-entries made
421
	 */
422
	protected function save_history(array $data,array $old=null,$deleted=null,array $changed_fields=null)
423
	{
424
		unset($deleted);	// not used, but required by function signature
425
426
		//error_log(__METHOD__.__LINE__.' Changedfields:'.array2string($changed_fields));
427
		if (is_null($changed_fields))
428
		{
429
			$changed_fields = self::changed_fields($data,$old);
0 ignored issues
show
Bug Best Practice introduced by
The method EGroupware\Api\Storage\Tracking::changed_fields() 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

429
			/** @scrutinizer ignore-call */ 
430
   $changed_fields = self::changed_fields($data,$old);
Loading history...
430
			//error_log(__METHOD__.__LINE__.' Changedfields:'.array2string($changed_fields));
431
		}
432
		if (!$changed_fields && ($old || !$GLOBALS['egw_info']['server']['log_user_agent_action'])) return 0;
433
434
		if (!is_object($this->historylog) || $this->historylog->user != $this->user)
435
		{
436
			$this->historylog = new History($this->app, $this->user);
437
		}
438
		// log user-agent and session-action
439
		if ($GLOBALS['egw_info']['server']['log_user_agent_action'] && ($changed_fields || !$old))
440
		{
441
			$this->historylog->add('user_agent_action', $data[$this->id_field],
442
				$_SERVER['HTTP_USER_AGENT'], $_SESSION[Api\Session::EGW_SESSION_VAR]['session_action']);
443
		}
444
		foreach($changed_fields as $name)
445
		{
446
			$status = isset($this->field2history[$name]) ? $this->field2history[$name] : $name;
447
			//error_log(__METHOD__.__LINE__." Name $name,".' Status:'.array2string($status));
448
			if (is_array($status))	// 1:N relation --> remove common rows
449
			{
450
				//error_log(__METHOD__.__LINE__.' is Array');
451
				self::compact_1_N_relation($data[$name],$status);
452
				self::compact_1_N_relation($old[$name],$status);
453
				$added = array_values(array_diff($data[$name],$old[$name]));
454
				$removed = array_values(array_diff($old[$name],$data[$name]));
455
				$n = max(array(count($added),count($removed)));
456
				for($i = 0; $i < $n; ++$i)
457
				{
458
					//error_log(__METHOD__."() $i: historylog->add('$name',data['$this->id_field']={$data[$this->id_field]},".array2string($added[$i]).','.array2string($removed[$i]));
459
					$this->historylog->add($name,$data[$this->id_field],$added[$i],$removed[$i]);
460
				}
461
			}
462
			else if (is_string($data[$name]) && is_string($old[$name]) && (
463
					$this->historylog->needs_diff ($name, $data[$name]) || $this->historylog->needs_diff ($name, $old[$name])))
464
			{
465
				// Multiline string, just store diff
466
				// Strip HTML first though
467
				$old_text = Api\Mail\Html::convertHTMLToText($old[$name]);
468
				$new_text = Api\Mail\Html::convertHTMLToText($data[$name]);
469
470
				// If only change was something in HTML, show the HTML
471
				if(trim($old_text) === trim($new_text))
472
				{
473
					$old_text = $old[$name];
474
					$new_text = $data[$name];
475
				}
476
477
				$diff = new \Horde_Text_Diff('auto', array(explode("\n",$old_text), explode("\n",$new_text)));
478
				$renderer = new \Horde_Text_Diff_Renderer_Unified();
479
				$this->historylog->add(
480
					$status,
481
					$data[$this->id_field],
482
					$renderer->render($diff),
483
					self::DIFF_MARKER
484
				);
485
			}
486
			else
487
			{
488
				//error_log(__METHOD__.__LINE__.' IDField:'.array2string($this->id_field).' ->'.$data[$this->id_field].' New:'.$data[$name].' Old:'.$old[$name]);
489
				$this->historylog->add($status,$data[$this->id_field],
490
					is_array($data[$name]) ? implode(',',$data[$name]) : $data[$name],
491
					is_array($old[$name]) ? implode(',',$old[$name]) : $old[$name]);
492
			}
493
		}
494
		//error_log(__METHOD__.__LINE__.' return:'.count($changed_fields));
495
		return count($changed_fields);
496
	}
497
498
	/**
499
	 * Compute changes between new and old data
500
	 *
501
	 * Can be used to check if saving the data is really necessary or user just pressed save
502
	 *
503
	 * @param array $data
504
	 * @param array $old = null
505
	 * @return array of keys with different values in $data and $old
506
	 */
507
	public function changed_fields(array $data,array $old=null)
508
	{
509
		if (is_null($old)) return array_keys($data);
510
		$changed_fields = array();
511
		foreach($this->field2history as $name => $status)
512
		{
513
			if (!$old[$name] && !$data[$name]) continue;	// treat all sorts of empty equally
514
515
			if ($name[0] == '#' && !isset($data[$name])) continue;	// no set customfields are not stored, therefore not changed
516
517
			if (is_array($status))	// 1:N relation
518
			{
519
				self::compact_1_N_relation($data[$name],$status);
520
				self::compact_1_N_relation($old[$name],$status);
521
			}
522
			if ($old[$name] != $data[$name])
523
			{
524
				// normalize arrays, we do NOT care for the order of multiselections
525
				if (is_array($data[$name]) || is_array($old[$name]))
526
				{
527
					if (!is_array($data[$name])) $data[$name] = explode(',',$data[$name]);
528
					if (!is_array($old[$name])) $old[$name] = explode(',',$old[$name]);
529
					if (count($data[$name]) == count($old[$name]))
530
					{
531
						sort($data[$name]);
532
						sort($old[$name]);
533
						if ($data[$name] == $old[$name]) continue;
534
					}
535
				}
536
				elseif (str_replace("\r", '', $old[$name]) == str_replace("\r", '', $data[$name]))
537
				{
538
					continue;	// change only in CR (eg. different OS) --> ignore
539
				}
540
				$changed_fields[] = $name;
541
				//echo "<p>$name: ".array2string($data[$name]).' != '.array2string($old[$name])."</p>\n";
542
			}
543
		}
544
		foreach($data as $name => $value)
545
		{
546
			if ($name[0] == '#' && $name[1] == '#' && $value !== $old[$name])
547
			{
548
				$changed_fields[] = $name;
549
			}
550
		}
551
		//error_log(__METHOD__."() changed_fields=".array2string($changed_fields));
552
		return $changed_fields;
553
	}
554
555
	/**
556
	 * Compact (spezified) fields of a 1:N relation into an array of strings
557
	 *
558
	 * @param array &$rows rows of the 1:N relation
559
	 * @param array $cols field names as values
560
	 */
561
	private static function compact_1_N_relation(&$rows,array $cols)
562
	{
563
		if (is_array($rows))
0 ignored issues
show
introduced by
The condition is_array($rows) is always true.
Loading history...
564
		{
565
			foreach($rows as &$row)
566
			{
567
				$values = array();
568
				foreach($cols as $col)
569
				{
570
					$values[] = $row[$col];
571
				}
572
				$row = implode(self::ONE2N_SEPERATOR,$values);
573
			}
574
		}
575
		else
576
		{
577
			$rows = array();
578
		}
579
	}
580
581
	/**
582
	 * sending all notifications for the changed entry
583
	 *
584
	 * @internal use only track($data,$old,$user)
585
	 * @param array $data current entry
586
	 * @param array $old = null old/last state of the entry or null for a new entry
587
	 * @param boolean $deleted = null can be set to true to let the tracking know the item got deleted or undelted
588
	 * @param array $email_notified=null if present will return the emails notified, if given emails in that list will not be notified
589
	 * @return boolean true on success, false on error (error messages are in $this->errors)
590
	 */
591
	public function do_notifications($data,$old,$deleted=null,&$email_notified=null)
592
	{
593
		$this->errors = $email_sent = array();
594
		if (!empty($email_notified) && is_array($email_notified)) $email_sent = $email_notified;
595
596
		if (!$this->notify_current_user && $this->user)		// do we have a current user and should we notify the current user about his own changes
597
		{
598
			//error_log("do_notificaton() adding user=$this->user to email_sent, to not notify him");
599
			$email_sent[] = $GLOBALS['egw']->accounts->id2name($this->user,'account_email');
600
		}
601
		$skip_notify = $this->get_config('skip_notify',$data,$old);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $skip_notify is correct as $this->get_config('skip_notify', $data, $old) targeting EGroupware\Api\Storage\Tracking::get_config() seems to always return null.

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

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

}

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

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

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

Loading history...
602
		if($skip_notify && is_array($skip_notify))
0 ignored issues
show
introduced by
$skip_notify is of type null, thus it always evaluated to false.
Loading history...
603
		{
604
			$email_sent = array_merge($email_sent, $skip_notify);
605
		}
606
607
		// entry creator
608
		if ($this->creator_field && ($email = $GLOBALS['egw']->accounts->id2name($data[$this->creator_field],'account_email')) &&
609
			!in_array($email, $email_sent))
610
		{
611
			if ($this->send_notification($data,$old,$email,$data[$this->creator_field],'notify_creator'))
612
			{
613
				$email_sent[] = $email;
614
			}
615
		}
616
617
		// members of group when entry owned by group
618
		if ($this->creator_field && $GLOBALS['egw']->accounts->get_type($data[$this->creator_field]) == 'g')
619
		{
620
			foreach($GLOBALS['egw']->accounts->members($data[$this->creator_field],true) as $u)
621
			{
622
				if (($email = $GLOBALS['egw']->accounts->id2name($u,'account_email')) &&
623
					!in_array($email, $email_sent))
624
				{
625
					if ($this->send_notification($data,$old,$email,$u,'notify_owner_group_member'))
626
					{
627
						$email_sent[] = $email;
628
					}
629
				}
630
			}
631
		}
632
633
		// assigned / responsible users
634
		if ($this->assigned_field || $assigned = $this->get_config('assigned', $data))
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $assigned is correct as $this->get_config('assigned', $data) targeting EGroupware\Api\Storage\Tracking::get_config() seems to always return null.

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

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

}

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

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

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

Loading history...
635
		{
636
			//error_log(__METHOD__."() data[$this->assigned_field]=".print_r($data[$this->assigned_field],true).", old[$this->assigned_field]=".print_r($old[$this->assigned_field],true));
637
			$old_assignees = array();
638
			$assignees = $assigned ? $assigned : array();
639
			if ($data[$this->assigned_field])	// current assignments
640
			{
641
				$assignees = is_array($data[$this->assigned_field]) ?
642
					$data[$this->assigned_field] : explode(',',$data[$this->assigned_field]);
643
			}
644
			if ($old && $old[$this->assigned_field])
0 ignored issues
show
Bug Best Practice introduced by
The expression $old 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...
645
			{
646
				$old_assignees = is_array($old[$this->assigned_field]) ?
647
					$old[$this->assigned_field] : explode(',',$old[$this->assigned_field]);
648
			}
649
			foreach(array_unique(array_merge($assignees,$old_assignees)) as $assignee)
650
			{
651
				//error_log(__METHOD__."() assignee=$assignee, type=".$GLOBALS['egw']->accounts->get_type($assignee).", email=".$GLOBALS['egw']->accounts->id2name($assignee,'account_email'));
652
				if (!$assignee) continue;
653
654
				// item assignee is a user
655
				if ($GLOBALS['egw']->accounts->get_type($assignee) == 'u')
656
				{
657
					if (($email = $GLOBALS['egw']->accounts->id2name($assignee,'account_email')) && !in_array($email, $email_sent))
658
					{
659
						if ($this->send_notification($data,$old,$email,$assignee,'notify_assigned',
660
							in_array($assignee,$assignees) !== in_array($assignee,$old_assignees) || $deleted))	// assignment changed
661
						{
662
							$email_sent[] = $email;
663
						}
664
					}
665
				}
666
				else	// item assignee is a group
667
				{
668
					foreach($GLOBALS['egw']->accounts->members($assignee,true) as $u)
669
					{
670
						if (($email = $GLOBALS['egw']->accounts->id2name($u,'account_email')) && !in_array($email, $email_sent))
671
						{
672
							if ($this->send_notification($data,$old,$email,$u,'notify_assigned',
673
								in_array($u,$assignees) !== in_array($u,$old_assignees) || $deleted))	// assignment changed
674
							{
675
								$email_sent[] = $email;
676
							}
677
						}
678
					}
679
				}
680
			}
681
		}
682
683
		// notification copies
684
		if (($copies = $this->get_config('copy',$data,$old)))
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $copies is correct as $this->get_config('copy', $data, $old) targeting EGroupware\Api\Storage\Tracking::get_config() seems to always return null.

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

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

}

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

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

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

Loading history...
685
		{
686
			$lang = $this->get_config('lang',$data,$old);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $lang is correct as $this->get_config('lang', $data, $old) targeting EGroupware\Api\Storage\Tracking::get_config() seems to always return null.

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

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

}

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

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

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

Loading history...
687
			foreach($copies as $email)
0 ignored issues
show
Bug introduced by
The expression $copies of type void is not traversable.
Loading history...
688
			{
689
				if (strchr($email,'@') !== false && !in_array($email, $email_sent))
690
				{
691
					if ($this->send_notification($data,$old,$email,$lang,'notify_copy'))
692
					{
693
						$email_sent[] = $email;
694
					}
695
				}
696
			}
697
		}
698
		$email_notified = $email_sent;
699
		return !count($this->errors);
700
	}
701
702
	/**
703
	 * Cache for notificaton body
704
	 *
705
	 * Cache is by id, language, date-format and type text/html
706
	 */
707
	protected $body_cache = array();
708
709
	/**
710
	 * method to clear the Cache for notificaton body
711
	 *
712
	 * Cache is by id, language, date-format and type text/html
713
	 */
714
	public function ClearBodyCache()
715
	{
716
		$this->body_cache = array();
717
	}
718
719
	/**
720
	 * Sending a notification to the given email-address
721
	 *
722
	 * Called by track() or externally for sending async notifications
723
	 *
724
	 * Method changes $GLOBALS['egw_info']['user'], so everything called by it, eg. get_(subject|body|links|attachements),
725
	 * must NOT store something from user enviroment! By the end of the method, everything get changed back.
726
	 *
727
	 * @param array $data current entry
728
	 * @param array $old = null old/last state of the entry or null for a new entry
729
	 * @param string $email address to send the notification to
730
	 * @param string $user_or_lang = 'en' user-id or 2 char lang-code for a non-system user
731
	 * @param string $check = null pref. to check if a notification is wanted
732
	 * @param boolean $assignment_changed = true the assignment of the user $user_or_lang changed
733
	 * @param boolean $deleted = null can be set to true to let the tracking know the item got deleted or undelted
734
	 * @return boolean true on success or false if notification not requested or error (error-message is in $this->errors)
735
	 */
736
	public function send_notification($data,$old,$email,$user_or_lang,$check=null,$assignment_changed=true,$deleted=null)
737
	{
738
		//error_log(__METHOD__."(,,'$email',$user_or_lang,$check,$assignment_changed,$deleted)");
739
		if (!$email) return false;
740
741
		$save_user = $GLOBALS['egw_info']['user'];
742
		$do_notify = true;
743
		$can_cache = false;
744
745
		if (is_numeric($user_or_lang))	// user --> read everything from his prefs
746
		{
747
			$GLOBALS['egw_info']['user']['account_id'] = $user_or_lang;
748
			$GLOBALS['egw']->preferences->__construct($user_or_lang);
749
			$GLOBALS['egw_info']['user']['preferences'] = $GLOBALS['egw']->preferences->read_repository(false);	// no session prefs!
750
751
			if ($check && $this->check2pref) $check = $this->check2pref[$check];
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->check2pref 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...
752
753
			if ($check && !$GLOBALS['egw_info']['user']['preferences'][$this->app][$check] ||	// no notification requested
754
				// only notification about changed assignment requested
755
				$check && $GLOBALS['egw_info']['user']['preferences'][$this->app][$check] === 'assignment' && !$assignment_changed ||
756
				$this->user == $user_or_lang && !$this->notify_current_user)  // no popup for own actions
757
			{
758
				$do_notify = false;	// no notification requested / necessary
759
			}
760
			$can_cache = (Customfields::get($this->app, true) == Customfields::get($this->app, $user_or_lang));
761
		}
762
		else
763
		{
764
			// for the notification copy, we use default (and forced) prefs plus the language from the the tracker config
765
			$GLOBALS['egw_info']['user']['preferences'] = $GLOBALS['egw']->preferences->default_prefs();
766
			$GLOBALS['egw_info']['user']['preferences']['common']['lang'] = $user_or_lang;
767
		}
768
		if ($GLOBALS['egw_info']['user']['preferences']['common']['lang'] != Api\Translation::$userlang)	// load the right language if needed
769
		{
770
			Api\Translation::init();
771
		}
772
773
		$receiver = is_numeric($user_or_lang) ? $user_or_lang : $email;
774
775
		if ($do_notify)
776
		{
777
			// Load date/time preferences into egw_time
778
			Api\DateTime::init();
779
780
			// Cache message body to not have to re-generate it every time
781
			$lang = Api\Translation::$userlang;
782
			$date_format = $GLOBALS['egw_info']['user']['preferences']['common']['dateformat'] .
783
				$GLOBALS['egw_info']['user']['preferences']['common']['timeformat'];
784
785
			// Cache text body, if there's no private custom fields we might reveal
786
			if($can_cache)
787
			{
788
				$body_cache =& $this->body_cache[$data[$this->id_field]][$lang][$date_format];
789
			}
790
			if(empty($data[$this->id_field]) || !isset($body_cache['text']))
791
			{
792
				$body_cache['text'] = $this->get_body(false,$data,$old,false,$receiver);
793
			}
794
			// Cache HTML body
795
			if(empty($data[$this->id_field]) || !isset($body_cache['html']))
796
			{
797
				$body_cache['html'] = $this->get_body(true,$data,$old,false,$receiver);
798
			}
799
800
			// get rest of notification message
801
			$sender = $this->get_sender($data,$old,true,$receiver);
802
			$reply_to = $this->get_reply_to($data,$old);
803
			$subject = $this->get_subject($data,$old,$deleted,$receiver);
804
			$link = $this->get_notification_link($data,$old,$receiver);
805
			$attachments = $this->get_attachments($data,$old,$receiver);
806
		}
807
808
		// restore user enviroment BEFORE calling notification class or returning
809
		$GLOBALS['egw_info']['user'] = $save_user;
810
		// need to call preferences constructor and read_repository, to set user timezone again
811
		$GLOBALS['egw']->preferences->__construct($GLOBALS['egw_info']['user']['account_id']);
812
		$GLOBALS['egw_info']['user']['preferences'] = $GLOBALS['egw']->preferences->read_repository(false);	// no session prefs!
813
814
		// Re-load date/time preferences
815
		Api\DateTime::init();
816
817
		if ($GLOBALS['egw_info']['user']['preferences']['common']['lang'] != Api\Translation::$userlang)
818
		{
819
			Api\Translation::init();
820
		}
821
822
		if (!$do_notify)
823
		{
824
			return false;
825
		}
826
827
		// send over notification_app
828
		if ($GLOBALS['egw_info']['apps']['notifications']['enabled'])
829
		{
830
			// send via notification_app
831
			try {
832
				$class = $this->notification_class;
833
				$notification = new $class();
834
				$notification->set_receivers(array($receiver));
835
				$notification->set_message($body_cache['text'], 'plain');
836
				// add our own signature to distinguish between original message and reply
837
				// part. (e.g.: in OL there's no reply quote)
838
				$body_cache['html'] = "<span style='display:none;'>-----".lang('original message')."-----</span>"."\r\n".$body_cache['html'];
839
				$notification->set_message($body_cache['html'], 'html');
840
				$notification->set_sender($sender);
841
				$notification->set_reply_to($reply_to);
842
				$notification->set_subject($subject);
843
				$notification->set_links(array($link));
844
				$notification->set_popupdata($link['app'], $link);
845
				if ($attachments && is_array($attachments))
846
				{
847
					$notification->set_attachments($attachments);
848
				}
849
				$notification->send();
850
851
				// Notification can (partially) succeed and still generate errors
852
				$this->errors += $notification->errors();
853
			}
854
			catch (Exception $exception)
0 ignored issues
show
Bug introduced by
The type EGroupware\Api\Storage\Exception was not found. Did you mean Exception? If so, make sure to prefix the type with \.
Loading history...
855
			{
856
				$this->errors[] = $exception->getMessage();
857
				return false;
858
			}
859
		}
860
		else
861
		{
862
			error_log('tracking: cannot send any notifications because notifications is not installed');
863
		}
864
865
		return true;
866
	}
867
868
	/**
869
	 * Return date+time formatted for the currently notified user (prefs in $GLOBALS['egw_info']['user']['preferences'])
870
	 *
871
	 * @param int|string|DateTime $timestamp in server-time
0 ignored issues
show
Bug introduced by
The type EGroupware\Api\Storage\DateTime was not found. Did you mean DateTime? If so, make sure to prefix the type with \.
Loading history...
872
	 * @param boolean $do_time =true true=allways (default), false=never print the time, null=print time if != 00:00
873
	 *
874
	 * @return string
875
	 */
876
	public function datetime($timestamp,$do_time=true)
877
	{
878
		if (!is_a($timestamp,'DateTime'))
879
		{
880
			$timestamp = new Api\DateTime($timestamp,Api\DateTime::$server_timezone);
881
		}
882
		$timestamp->setTimezone(Api\DateTime::$user_timezone);
883
		if (is_null($do_time))
0 ignored issues
show
introduced by
The condition is_null($do_time) is always false.
Loading history...
884
		{
885
			$do_time = ($timestamp->format('Hi') != '0000');
886
		}
887
		$format = $GLOBALS['egw_info']['user']['preferences']['common']['dateformat'];
888
		if ($do_time) $format .= ' '.($GLOBALS['egw_info']['user']['preferences']['common']['timeformat'] != 12 ? 'H:i' : 'h:i a');
889
890
		return $timestamp->format($format);
891
	}
892
893
	/**
894
	 * Get sender address
895
	 *
896
	 * The default implementation prefers depending on the prefer_user_as_sender class-var the user over
897
	 * what is returned by get_config('sender').
898
	 *
899
	 * @param int $user account_lid of user
900
	 * @param array $data
901
	 * @param array $old
902
	 * @param bool $prefer_id returns the userid rather than email
903
	 * @param int|string $receiver nummeric account_id or email address
904
	 * @return string or userid
905
	 */
906
	protected function get_sender($data,$old,$prefer_id=false,$receiver=null)
907
	{
908
		unset($receiver);	// not used, but required by function signature
909
910
		$sender = $this->get_config('sender',$data,$old);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $sender is correct as $this->get_config('sender', $data, $old) targeting EGroupware\Api\Storage\Tracking::get_config() seems to always return null.

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

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

}

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

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

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

Loading history...
911
		//echo "<p>".__METHOD__."() get_config('sender',...)='".htmlspecialchars($sender)."'</p>\n";
912
913
		if (($this->prefer_user_as_sender || !$sender) && $this->user &&
0 ignored issues
show
introduced by
$sender is of type null, thus it always evaluated to false.
Loading history...
914
			($email = $GLOBALS['egw']->accounts->id2name($this->user,'account_email')))
915
		{
916
			$name = $GLOBALS['egw']->accounts->id2name($this->user,'account_fullname');
917
918
			if($prefer_id) {
919
				$sender = $this->user;
920
			} else {
921
				$sender = $name ? $name.' <'.$email.'>' : $email;
922
			}
923
		}
924
		elseif(!$sender)
0 ignored issues
show
introduced by
$sender is of type null, thus it always evaluated to false.
Loading history...
925
		{
926
			$sender = 'EGroupware '.lang($this->app).' <noreply@'.$GLOBALS['egw_info']['server']['mail_suffix'].'>';
927
		}
928
		//echo "<p>".__METHOD__."()='".htmlspecialchars($sender)."'</p>\n";
929
		return $sender;
930
	}
931
932
	/**
933
	 * Get reply to address
934
	 *
935
	 * The default implementation prefers depending on what is returned by get_config('reply_to').
936
	 *
937
	 * @param int $user account_lid of user
938
	 * @param array $data
939
	 * @param array $old
940
	 * @return string or null
941
	 */
942
	protected function get_reply_to($data,$old)
943
	{
944
		$reply_to = $this->get_config('reply_to',$data,$old);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $reply_to is correct as $this->get_config('reply_to', $data, $old) targeting EGroupware\Api\Storage\Tracking::get_config() seems to always return null.

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

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

}

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

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

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

Loading history...
945
946
		return $reply_to;
947
	}
948
949
	/**
950
	 * Get the title for a given entry, can be reimplemented
951
	 *
952
	 * @param array $data
953
	 * @param array $old
954
	 * @return string
955
	 */
956
	protected function get_title($data,$old)
957
	{
958
		unset($old);	// not used, but required by function signature
959
960
		return Api\Link::title($this->app,$data[$this->id_field]);
961
	}
962
963
	/**
964
	 * Get the subject for a given entry, can be reimplemented
965
	 *
966
	 * Default implementation uses the link-title
967
	 *
968
	 * @param array $data
969
	 * @param array $old
970
	 * @param boolean $deleted =null can be set to true to let the tracking know the item got deleted or undelted
971
	 * @param int|string $receiver nummeric account_id or email address
972
	 * @return string
973
	 */
974
	protected function get_subject($data,$old,$deleted=null,$receiver=null)
975
	{
976
		unset($old, $deleted, $receiver);	// not used, but required by function signature
977
978
		return Api\Link::title($this->app,$data[$this->id_field]);
979
	}
980
981
	/**
982
	 * Get the modified / new message (1. line of mail body) for a given entry, can be reimplemented
983
	 *
984
	 * Default implementation does nothing
985
	 *
986
	 * @param array $data
987
	 * @param array $old
988
	 * @param int|string $receiver nummeric account_id or email address
989
	 * @return string
990
	 */
991
	protected function get_message($data,$old,$receiver=null)
992
	{
993
		unset($data, $old, $receiver);	// not used, but required by function signature
994
995
		return '';
996
	}
997
998
	/**
999
	 * Get a link to view the entry, can be reimplemented
1000
	 *
1001
	 * Default implementation checks get_config('link') (appending the id) or link::view($this->app,$id)
1002
	 *
1003
	 * @param array $data
1004
	 * @param array $old
1005
	 * @param string $allow_popup = false if true return array(link,popup-size) incl. session info an evtl. partial url (no host-part)
1006
	 * @param int|string $receiver nummeric account_id or email address
1007
	 * @return string|array string with link (!$allow_popup) or array(link,popup-size), popup size is something like '640x480'
1008
	 */
1009
	protected function get_link($data,$old,$allow_popup=false,$receiver=null)
1010
	{
1011
		unset($receiver);	// not used, but required by function signature
1012
1013
		if (($link = $this->get_config('link',$data,$old)))
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $link is correct as $this->get_config('link', $data, $old) targeting EGroupware\Api\Storage\Tracking::get_config() seems to always return null.

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

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

}

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

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

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

Loading history...
1014
		{
1015
			if (!$this->get_config('link_no_id', $data) && strpos($link,$this->id_field.'=') === false && isset($data[$this->id_field]))
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->get_config('link_no_id', $data) targeting EGroupware\Api\Storage\Tracking::get_config() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

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

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

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

Loading history...
1016
			{
1017
				$link .= strpos($link,'?') === false ? '?' : '&';
1018
				$link .= $this->id_field.'='.$data[$this->id_field];
1019
			}
1020
		}
1021
		else
1022
		{
1023
			if (($view = Api\Link::view($this->app,$data[$this->id_field])))
1024
			{
1025
				$link = Api\Framework::link('/index.php',$view);
1026
				$popup = Api\Link::is_popup($this->app,'view');
1027
			}
1028
		}
1029
		if ($link[0] == '/') Api\Framework::getUrl($link);
1030
1031
		if (!$allow_popup)
1032
		{
1033
			// remove the session-id in the notification mail!
1034
			$link = preg_replace('/(sessionid|kp3|domain)=[^&]+&?/','',$link);
1035
1036
			if ($popup) $link .= '&nopopup=1';
1037
		}
1038
		//error_log(__METHOD__."(..., $allow_popup, $receiver) returning ".array2string($allow_popup ? array($link,$popup) : $link));
1039
		return $allow_popup ? array($link,$popup) : $link;
1040
	}
1041
1042
	/**
1043
	 * Get a link for notifications to view the entry, can be reimplemented
1044
	 *
1045
	 * @param array $data
1046
	 * @param array $old
1047
	 * @param int|string $receiver nummeric account_id or email address
1048
	 * @return array with link
1049
	 */
1050
	protected function get_notification_link($data,$old,$receiver=null)
1051
	{
1052
		unset($receiver);	// not used, but required by function signature
1053
1054
		if (($view = Api\Link::view($this->app,$data[$this->id_field])))
1055
		{
1056
			return array(
1057
				'text' 	=> $this->get_title($data,$old),
1058
				'app'	=> $this->app,
1059
				'id'	=> $data[$this->id_field],
1060
				'view' 	=> $view,
1061
				'popup'	=> Api\Link::is_popup($this->app,'view'),
1062
			);
1063
		}
1064
		return false;
1065
	}
1066
1067
	/**
1068
	 * Get the body of the notification message, can be reimplemented
1069
	 *
1070
	 * @param boolean $html_email
1071
	 * @param array $data
1072
	 * @param array $old
1073
	 * @param boolean $integrate_link to have links embedded inside the body
1074
	 * @param int|string $receiver nummeric account_id or email address
1075
	 * @return string
1076
	 */
1077
	public function get_body($html_email,$data,$old,$integrate_link = true,$receiver=null)
1078
	{
1079
		$body = '';
1080
		if($this->get_config(self::CUSTOM_NOTIFICATION, $data, $old))
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->get_config(self::...IFICATION, $data, $old) targeting EGroupware\Api\Storage\Tracking::get_config() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

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

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

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

Loading history...
1081
		{
1082
			$body = $this->get_custom_message($data,$old,null,$receiver);
1083
			if(($sig = $this->get_signature($data,$old,$receiver)))
1084
			{
1085
				$body .= ($html_email ? '<br />':'') . "\n$sig";
1086
			}
1087
			return $body;
1088
		}
1089
		if ($html_email)
1090
		{
1091
			$body = '<table cellspacing="2" cellpadding="0" border="0" width="100%">'."\n";
1092
		}
1093
		// new or modified message
1094
		if (($message = $this->get_message($data,$old,$receiver)))
1095
		{
1096
			foreach ((array)$message as $_message)
1097
			{
1098
				$body .= $this->format_line($html_email,'message',false,($_message=='---'?($html_email?'<hr/>':$_message):$_message));
1099
			}
1100
		}
1101
		if ($integrate_link && ($link = $this->get_link($data,$old,false,$receiver)))
1102
		{
1103
			$body .= $this->format_line($html_email,'link',false,$integrate_link === true ? lang('You can respond by visiting:') : $integrate_link,$link);
0 ignored issues
show
introduced by
The condition $integrate_link === true is always true.
Loading history...
1104
		}
1105
		foreach($this->get_details($data,$receiver) as $name => $detail)
1106
		{
1107
			// if there's no old entry, the entry is not modified by definition
1108
			// if both values are '', 0 or null, we count them as equal too
1109
			$modified = $old && $data[$name] != $old[$name] && !(!$data[$name] && !$old[$name]);
1110
			//if ($modified) error_log("data[$name]=".print_r($data[$name],true).", old[$name]=".print_r($old[$name],true)." --> modified=".(int)$modified);
1111
			if (empty($detail['value']) && !$modified) continue;	// skip unchanged, empty values
1112
1113
			$body .= $this->format_line($html_email,$detail['type'],$modified,
1114
				$detail['label'] ? $detail['label'] : '', $detail['value']);
1115
		}
1116
		if ($html_email)
1117
		{
1118
			$body .= "</table>\n";
1119
		}
1120
		if(($sig = $this->get_signature($data,$old,$receiver)))
1121
		{
1122
			$body .= ($html_email ? '<br />':'') . "\n$sig";
1123
		}
1124
		if (!$html_email && $data['tr_edit_mode'] == 'html')
1125
		{
1126
			$body = Api\Mail\Html::convertHTMLToText($body);
1127
		}
1128
		return $body;
1129
	}
1130
1131
	/**
1132
	 * Format one line to the mail body
1133
	 *
1134
	 * @internal
1135
	 * @param boolean $html_mail
1136
	 * @param string $type 'link', 'message', 'summary', 'multiline', 'reply' and ''=regular content
1137
	 * @param boolean $modified mark field as modified
1138
	 * @param string $line whole line or just label
1139
	 * @param string $data = null data or null to display just $line over 2 columns
1140
	 * @return string
1141
	 */
1142
	protected function format_line($html_mail,$type,$modified,$line,$data=null)
1143
	{
1144
		//error_log(__METHOD__.'('.array2string($html_mail).",'$type',".array2string($modified).",'$line',".array2string($data).')');
1145
		$content = '';
1146
1147
		if ($html_mail)
1148
		{
1149
			if (!$this->html_content_allow) $line = Api\Html::htmlspecialchars($line);	// XSS
1150
1151
			$color = $modified ? 'red' : false;
1152
			$size  = '110%';
1153
			$bold = false;
1154
			$background = '#FFFFF1';
1155
			switch($type)
1156
			{
1157
				case 'message':
1158
					$background = '#D3DCE3;';
1159
					$bold = true;
1160
					break;
1161
				case 'link':
1162
					$background = '#F1F1F1';
1163
					break;
1164
				case 'summary':
1165
					$background = '#F1F1F1';
1166
					$bold = true;
1167
					break;
1168
				case 'multiline':
1169
					// Only Convert nl2br on non-html content
1170
					if (strpos($data, '<br') === false)
1171
					{
1172
						$data = nl2br($this->html_content_allow ? $data : Api\Html::htmlspecialchars($data));
1173
						$this->html_content_allow = true;	// to NOT do htmlspecialchars again
1174
					}
1175
					break;
1176
				case 'reply':
1177
					$background = '#F1F1F1';
1178
					break;
1179
				default:
1180
					$size = false;
1181
			}
1182
			$style = ($bold ? 'font-weight:bold;' : '').($size ? 'font-size:'.$size.';' : '').($color?'color:'.$color:'');
1183
1184
			$content = '<tr style="background-color: '.$background.';"><td style="'.$style.($line && $data?'" width="20%':'" colspan="2').'">';
1185
		}
1186
		else	// text-mail
1187
		{
1188
			if ($type == 'reply') $content = str_repeat('-',64)."\n";
1189
1190
			if ($modified) $content .= '> ';
1191
		}
1192
		$content .= $line;
1193
1194
		if ($html_mail)
1195
		{
1196
			if ($line && $data) $content .= '</td><td style="'.$style.'">';
1197
			if ($type == 'link')
1198
			{
1199
				// the link is often too long for html boxes chunk-split allows to break lines if needed
1200
				$content .= Api\Html::a_href(chunk_split(rawurldecode($data),40,'&#8203;'),$data,'','target="_blank"');
1201
			}
1202
			elseif ($this->html_content_allow)
1203
			{
1204
				$content .= Api\Html::activate_links($data);
1205
			}
1206
			else
1207
			{
1208
				$content .= Api\Html::htmlspecialchars($data);
1209
			}
1210
		}
1211
		else
1212
		{
1213
			$content .= ($content&&$data?': ':'').$data;
1214
		}
1215
		if ($html_mail) $content .= '</td></tr>';
1216
1217
		$content .= "\n";
1218
1219
		return $content;
1220
	}
1221
1222
	/**
1223
	 * Get the attachments for a notification
1224
	 *
1225
	 * @param array $data
1226
	 * @param array $old
1227
	 * @param int|string $receiver nummeric account_id or email address
1228
	 * @return array or array with values for either 'string' or 'path' and optionally (mime-)'type', 'filename' and 'encoding'
1229
	 */
1230
	protected function get_attachments($data,$old,$receiver=null)
1231
	{
1232
		unset($data, $old, $receiver);	// not used, but required by function signature
1233
1234
	 	return array();
1235
	}
1236
1237
	/**
1238
	 * Get a (global) signature to append to the change notificaiton
1239
	 * @param array $data
1240
	 * @param type $old
1241
	 * @param type $receiver
0 ignored issues
show
Bug introduced by
The type EGroupware\Api\Storage\type was not found. Maybe you did not declare it correctly or list all dependencies?

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

filter:
    dependency_paths: ["lib/*"]

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

Loading history...
1242
	 */
1243
	protected function get_signature($data, $old, $receiver)
1244
	{
1245
		unset($old, $receiver);	// not used, but required by function signature
1246
1247
		$config = Api\Config::read('notifications');
1248
		if(!isset($data[$this->id_field]))
1249
		{
1250
			error_log($this->app . ' did not properly implement bo_tracking->id_field.  Merge skipped.');
1251
		}
1252
		elseif(class_exists($this->app. '_merge'))
1253
		{
1254
			$merge_class = $this->app.'_merge';
1255
			$merge = new $merge_class();
1256
			$error = null;
1257
			$sig = $merge->merge_string($config['signature'], array($data[$this->id_field]), $error, 'text/html');
1258
			if($error)
0 ignored issues
show
introduced by
$error is of type null, thus it always evaluated to false.
Loading history...
1259
			{
1260
				error_log($error);
1261
				return $config['signature'];
1262
			}
1263
			return $sig;
1264
		}
1265
		return $config['signature'];
1266
	}
1267
1268
	/**
1269
	 * Get a custom notification message to be used instead of the standard one.
1270
	 * It can use merge print placeholders to include data.
1271
	 */
1272
	protected function get_custom_message($data, $old, $merge_class = null, $receiver = false)
1273
	{
1274
		$message = $this->get_config(self::CUSTOM_NOTIFICATION, $data, $old);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $message is correct as $this->get_config(self::...IFICATION, $data, $old) targeting EGroupware\Api\Storage\Tracking::get_config() seems to always return null.

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

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

}

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

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

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

Loading history...
1275
		if(!$message)
0 ignored issues
show
introduced by
$message is of type null, thus it always evaluated to false.
Loading history...
1276
		{
1277
			return '';
1278
		}
1279
1280
		// Check if there's any custom field privacy issues, and try to remove them
1281
		$message = $this->sanitize_custom_message($message, $receiver);
1282
1283
		// Automatically set merge class from naming conventions
1284
		if($merge_class == null)
1285
		{
1286
			$merge_class = $this->app.'_merge';
1287
		}
1288
		if(!isset($data[$this->id_field]))
1289
		{
1290
			error_log($this->app . ' did not properly implement bo_tracking->id_field.  Merge skipped.');
1291
			return $message;
1292
		}
1293
		elseif(class_exists($merge_class))
1294
		{
1295
			$merge = new $merge_class();
1296
			$error = null;
1297
			$merged_message = $merge->merge_string($message, array($data[$this->id_field]), $error, 'text/html');
1298
			if($error)
1299
			{
1300
				error_log($error);
1301
				return $message;
1302
			}
1303
			return $merged_message;
1304
		}
1305
		else
1306
		{
1307
			throw new Api\Exception\WrongParameter("Invalid merge class '$merge_class' for {$this->app} custom notification");
1308
		}
1309
	}
1310
1311
	/**
1312
	 * Check to see if the message would expose any custom fields that are
1313
	 * not visible to the receiver, and try to remove them from the message.
1314
	 *
1315
	 * @param string $message
1316
	 * @param string|int $receiver Account ID or email address
1317
	 */
1318
	protected function sanitize_custom_message($message, $receiver)
1319
	{
1320
		if(!is_numeric($receiver))
1321
		{
1322
			$receiver = false;
1323
		}
1324
1325
		$cfs = Customfields::get($this->app, $receiver);
1326
		$all_cfs = Customfields::get($this->app, true);
1327
1328
		// If we have a specific user and they're the same then there are
1329
		// no private fields so nothing needs to be done
1330
		if($receiver && $all_cfs == $cfs)
1331
		{
1332
			return $message;
1333
		}
1334
1335
		// Replace any placeholders that use the private field, or any sub-keys
1336
		// of the field
1337
		foreach($all_cfs as $name => $field)
1338
		{
1339
			if ($receiver === false && $field['private'] || !$cfs[$name])
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($receiver === false && ...ate']) || ! $cfs[$name], Probably Intended Meaning: $receiver === false && (...ate'] || ! $cfs[$name])
Loading history...
1340
			{
1341
				// {{field}} or {{field/subfield}} or $$field$$ or $$field/subfield$$
1342
				$message = preg_replace('/(\{\{|\$\$)#'.$name.'(\/[^\}\$]+)?(\}\}|\$\$)/', '', $message);
1343
			}
1344
		}
1345
		return $message;
1346
	}
1347
}
1348