calendar_uilist::action()   F
last analyzed

Complexity

Conditions 64

Size

Total Lines 277
Code Lines 152

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 64
eloc 152
nop 9
dl 0
loc 277
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 - Calendar's Listview and Search
4
 *
5
 * @link http://www.egroupware.org
6
 * @package calendar
7
 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
8
 * @copyright (c) 2005-16 by RalfBecker-At-outdoor-training.de
9
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
10
 * @version $Id$
11
 */
12
13
use EGroupware\Api;
14
use EGroupware\Api\Acl;
15
use EGroupware\Api\Etemplate;
16
use EGroupware\Api\Framework;
17
use EGroupware\Api\Link;
18
19
/**
20
 * Class to generate the calendar listview and the search
21
 *
22
 * The new UI, BO and SO classes have a strikt definition, in which time-zone they operate:
23
 *  UI only operates in user-time, so there have to be no conversation at all !!!
24
 *  BO's functions take and return user-time only (!), they convert internaly everything to servertime, because
25
 *  SO operates only on server-time
26
 *
27
 * The state of the UI elements is managed in the uical class, which all UI classes extend.
28
 *
29
 * All permanent debug messages of the calendar-code should done via the debug-message method of the bocal class !!!
30
 */
31
class calendar_uilist extends calendar_ui
32
{
33
	var $public_functions = array(
34
		'listview'  => True,
35
	);
36
	/**
37
	 * integer level or string function- or widget-name
38
	 *
39
	 * @var mixed
40
	 */
41
	var $debug=false;
42
	/**
43
	 * Filternames
44
	 *
45
	 * @var array
46
	 */
47
	var $date_filters = array(
48
		'after'  => 'After current date',
49
		'before' => 'Before current date',
50
		'today'  => 'Today',
51
		'week'   => 'Week',
52
		'month'  => 'Month',
53
		'all'	=> 'All events',
54
		'custom' => 'Selected range',
55
	);
56
57
	/**
58
	 * Constructor
59
	 *
60
	 * @param array $set_states =null to manualy set / change one of the states, default NULL = use $_REQUEST
61
	 */
62
	function __construct($set_states=null)
63
	{
64
		parent::__construct(true,$set_states);	// call the parent's constructor
65
66
		foreach($this->date_filters as $name => $label)
67
		{
68
			$this->date_filters[$name] = lang($label);
69
		}
70
71
		$this->check_owners_access();
72
	}
73
74
	/**
75
	 * Show the listview
76
	 */
77
	function listview($_content=null,$msg='',$home=false)
78
	{
79
		if ($_GET['msg']) $msg .= $_GET['msg'];
80
		if ($this->group_warning) $msg .= $this->group_warning;
0 ignored issues
show
Bug Best Practice introduced by
The property group_warning does not exist on calendar_uilist. Did you maybe forget to declare it?
Loading history...
81
82
		$etpl = new Etemplate('calendar.list');
83
84
		// Handle merge from sidebox
85
		if($_GET['merge'])
86
		{
87
			$_content['nm']['action'] = 'document_'.$_GET['merge'];
88
			$_content['nm']['select_all'] = true;
89
		}
90
91
		if (is_array($_content))
92
		{
93
			// handle a single button like actions
94
			foreach(array('delete','timesheet','document') as $button)
95
			{
96
				if ($_content['nm']['rows'][$button])
97
				{
98
					$id = key($_content['nm']['rows'][$button]);
99
					$_content['nm']['action'] = $button;
100
					$_content['nm']['selected'] = array($id);
101
				}
102
			}
103
			// Handle actions
104
			if ($_content['nm']['action'])
105
			{
106
				// Allow merge using the date range filter
107
				if(strpos($_content['nm']['action'],'document') !== false &&
108
					!count($_content['nm']['selected']) && !$_content['nm']['select_all']) {
109
					$_content['nm']['selected'][] = $this->get_merge_range($_content['nm']);
110
				}
111
				if (!count($_content['nm']['selected']) && !$_content['nm']['select_all'])
112
				{
113
					$msg = lang('You need to select some events first');
114
				}
115
				else
116
				{
117
					$success = $failed = $action_msg = null;
118
					if ($this->action($_content['nm']['action'],$_content['nm']['selected'],$_content['nm']['select_all'],
119
						$success,$failed,$action_msg,'calendar_list',$msg, $_content['nm']['checkboxes']['no_notifications']))
120
					{
121
						$msg .= lang('%1 event(s) %2',$success,$action_msg);
0 ignored issues
show
Unused Code introduced by
The call to lang() has too many arguments starting with $success. ( Ignorable by Annotation )

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

121
						$msg .= /** @scrutinizer ignore-call */ lang('%1 event(s) %2',$success,$action_msg);

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...
122
					}
123
					elseif(is_null($msg))
124
					{
125
						$msg .= lang('%1 event(s) %2, %3 failed because of insufficient rights !!!',$success,$action_msg,$failed);
126
					}
127
				}
128
			}
129
		}
130
		$content = array(
131
			'nm'  => Api\Cache::getSession('calendar', 'calendar_list'),
132
		);
133
		if (!is_array($content['nm']))
134
		{
135
			$content['nm'] = array(
136
				'get_rows'        =>	'calendar.calendar_uilist.get_rows',
137
	 			'filter_no_lang'  => True,	// I  set no_lang for filter (=dont translate the options)
138
				'no_filter2'      => True,	// I  disable the 2. filter (params are the same as for filter)
139
				'no_cat'          => True,	// I  disable the cat-selectbox
140
				'filter'          => 'month',
141
				'order'           => 'cal_start',// IO name of the column to sort after (optional for the sortheaders)
142
				'sort'            => 'ASC',// IO direction of the sort: 'ASC' or 'DESC'
143
				'default_cols'    => '!week,weekday,cal_title,cal_description,recure,cal_location,cal_owner,cat_id,pm_id',
144
				'filter_onchange' => "app.calendar.filter_change",
145
				'row_id'          => 'row_id',	// set in get rows "$event[id]:$event[recur_date]"
146
				'row_modified'    => 'modified',
147
				'favorites'       => true,
148
				'placeholder_actions' => array('add')
149
			);
150
		}
151
		$content['nm']['actions'] = $this->get_actions();
152
153
		// Skip first load if view is not listview
154
		if($this->view && $this->view !== 'listview')
155
		{
156
			$content['nm']['num_rows'] = 0;
157
		}
158
159
		if (isset($_GET['filter']) && in_array($_GET['filter'],array_keys($this->date_filters)))
160
		{
161
			$content['nm']['filter'] = $_GET['filter'];
162
		}
163
		if ($_GET['search'])
164
		{
165
			$content['nm']['search'] = $_GET['search'];
166
		}
167
		if($this->owner)
168
		{
169
			$content['nm']['col_filter']['participant'] = is_array($this->owner) ? $this->owner : explode(',',$this->owner);
0 ignored issues
show
introduced by
The condition is_array($this->owner) is always false.
Loading history...
170
		}
171
		// search via jdots ajax_exec uses $_REQUEST['json_data'] instead of regular GET parameters
172
		if (isset($_REQUEST['json_data']) && ($json_data = json_decode($_REQUEST['json_data'], true)) &&
173
			!empty($json_data['request']['parameters'][0]))
174
		{
175
			$params = null;
176
			parse_str(substr($json_data['request']['parameters'][0], 10), $params);	// cut off "/index.php?"
177
			if (isset($params['keywords']))	// new search => set filters so every match is shown
178
			{
179
				$this->adjust_for_search($params['keywords'], $content['nm']);
180
			}
181
			unset($params['keywords']);
182
		}
183
		if (isset($_REQUEST['keywords']))	// new search => set filters so every match is shown
184
		{
185
			$this->adjust_for_search($_REQUEST['keywords'],$content['nm']);
186
			unset($_REQUEST['keywords']);
187
		}
188
		$sel_options['filter'] = &$this->date_filters;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$sel_options was never initialized. Although not strictly required by PHP, it is generally a good practice to add $sel_options = array(); before regardless.
Loading history...
189
190
		// Send categories for row styling - calendar uses no_cat, so they don't go automatically
191
		$sel_options['category'] = array('' => lang('all')) + Etemplate\Widget\Select::typeOptions('select-cat', ',,calendar');
192
		// Prevent double encoding - widget does this on its own, but we're just grabbing the options
193
		foreach($sel_options['category'] as &$label)
194
		{
195
			if(!is_array($label))
196
			{
197
				$label = html_entity_decode($label, ENT_NOQUOTES,'utf-8');
198
			}
199
			elseif($label['label'])
200
			{
201
				$label['label'] = html_entity_decode($label['label'], ENT_NOQUOTES,'utf-8');
202
			}
203
		}
204
205
		// add scrollbar to long describtion, if user choose so in his prefs
206
		if ($this->prefs['limit_des_lines'] > 0 || (string)$this->prefs['limit_des_lines'] == '')
0 ignored issues
show
Bug Best Practice introduced by
The property prefs does not exist on calendar_uilist. Did you maybe forget to declare it?
Loading history...
207
		{
208
			$content['css'] .= '<style type="text/css">@media screen { .listDescription {  max-height: '.
209
				(($this->prefs['limit_des_lines'] ? $this->prefs['limit_des_lines'] : 5) * 1.35).	   // dono why em is not real lines
210
				'em; overflow: auto; }}</style>';
211
		}
212
213
		if($msg)
214
		{
215
			Framework::message($msg);
216
		}
217
		$html = $etpl->exec('calendar.calendar_uilist.listview',$content,$sel_options,array(),array(),$home ? -1 : 0);
218
219
		// Not sure why this has to be echoed instead of appended, but that's what works.
220
		//echo calendar_uiviews::edit_series();
221
222
		return $html;
223
	}
224
225
	/**
226
	 * set filter for search, so that everything is shown
227
	 */
228
	function adjust_for_search($keywords,&$params)
229
	{
230
		$params['search'] = $keywords;
231
		$params['start']  = 0;
232
		$params['order'] = 'cal_start';
233
		if ($keywords)
234
		{
235
			$params['sort'] = 'DESC';
236
			unset($params['col_filter']['participant']);
237
		}
238
		else
239
		{
240
			$params['sort'] = 'ASC';
241
		}
242
	}
243
244
	/**
245
	 * query calendar for nextmatch in the listview
246
	 *
247
	 * @internal
248
	 * @param array &$params parameters
249
	 * @param array &$rows returned rows/events
250
	 * @param array &$readonlys eg. to disable buttons based on Acl
251
	 */
252
	function get_rows(&$params,&$rows,&$readonlys)
253
	{
254
		unset($readonlys);	// not used;
255
		//echo "uilist::get_rows() params="; _debug_array($params);
256
		$this->filter = $params['filter'];
0 ignored issues
show
Bug Best Practice introduced by
The property filter does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
257
		if ($params['filter'] == 'custom')
258
		{
259
			if (!$params['startdate'] && !$params['enddate'])
260
			{
261
				$this->filter = 'all';
262
			}
263
			elseif (!$params['startdate'])
264
			{
265
				$this->filter = 'before';
266
				$this->manage_states(array('date' => $this->bo->date2string($params['enddate'])));
267
			}
268
			elseif (!$params['enddate'])
269
			{
270
				$this->filter = 'after';
271
				$this->manage_states(array('date' => $this->bo->date2string($params['startdate'])));
272
			}
273
		}
274
		$old_params = Api\Cache::getSession('calendar', 'calendar_list');
275
		if (is_array($old_params))
276
		{
277
			if ($old_params['filter'] && $old_params['filter'] != $params['filter'])	// filter changed => order accordingly
278
			{
279
				$params['order'] = 'cal_start';
280
				$params['sort'] = $params['filter'] == 'before' ? 'DESC' : 'ASC';
281
			}
282
			if ($old_params['search'] != $params['search'])
283
			{
284
				$this->adjust_for_search($params['search'],$params);
285
				$this->filter = $params['filter'];
286
			}
287
		}
288
289
		if (!$params['csv_export'])
290
		{
291
			Api\Cache::setSession('calendar', 'calendar_list',
292
				array_diff_key ($params, array_flip(array('rows', 'actions', 'action_links', 'placeholder_actions'))));
293
		}
294
295
		// release session to allow parallel requests to run
296
		$GLOBALS['egw']->session->commit_session();
297
298
		// do we need to query custom fields and which
299
		// Check stored preference if selectcols isn't available (ie: first call)
300
		$select_cols = $params['selectcols'] ? $params['selectcols'] : $GLOBALS['egw_info']['user']['preferences']['calendar']['nextmatch-calendar.list.rows'];
301
		if(!is_array($params['selectcols']))
302
		{
303
			$select_cols = explode(',',$select_cols);
304
		}
305
		if (in_array('cfs',$select_cols))
306
		{
307
			$cfs = array();
308
			foreach($select_cols as $col)
309
			{
310
				if ($col[0] == '#') $cfs[] = substr($col,1);
311
			}
312
		}
313
		$search_params = array(
314
			'cat_id'  => $params['cat_id'] ? $params['cat_id'] : 0,
315
			'filter'  => $this->filter,
316
			'query'   => $params['search'],
317
			'offset'  => (int) $params['start'],
318
			'num_rows'=> $params['num_rows'],
319
			'order'   => $params['order'] ? $params['order'].' '.$params['sort'] : 'cal_start ASC',
320
			'cfs'	 => $params['csv_export'] ? array() : $cfs,
321
		);
322
		// Non-blocking events above blocking
323
		$search_params['order'] .= ', cal_non_blocking DESC';
324
325
		switch($this->filter)
326
		{
327
			case 'all':
328
				break;
329
			case 'before':
330
				$search_params['end'] = $params['date'] ? Api\DateTime::to($params['date'],'ts') : $this->date;
331
				$label = lang('Before %1',$this->bo->long_date($search_params['end']));
0 ignored issues
show
Unused Code introduced by
The call to lang() has too many arguments starting with $this->bo->long_date($search_params['end']). ( Ignorable by Annotation )

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

331
				$label = /** @scrutinizer ignore-call */ lang('Before %1',$this->bo->long_date($search_params['end']));

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...
332
				break;
333
			case 'custom':
334
				$this->first = $search_params['start'] = Api\DateTime::to($params['startdate'],'ts');
0 ignored issues
show
Documentation Bug introduced by
It seems like $search_params['start'] ...ams['startdate'], 'ts') of type EGroupware\Api\datetime is incompatible with the declared type integer of property $first.

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...
335
				$this->last  = $search_params['end'] = strtotime('+1 day', $this->bo->date2ts($params['enddate']))-1;
336
				$label = $this->bo->long_date($this->first,$this->last);
337
				break;
338
			case 'today':
339
				$today = new Api\DateTime();
340
				$today->setTime(0, 0, 0);
341
				$this->first = $search_params['start'] = $today->format('ts');
342
				$today->setTime(23,59,59);
343
				$this->last  = $search_params['end'] = $today->format('ts');
0 ignored issues
show
Documentation Bug introduced by
It seems like $search_params['end'] = $today->format('ts') of type EGroupware\Api\DateTime is incompatible with the declared type integer of property $last.

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...
344
				break;
345
			case 'week':
346
				$start = new Api\DateTime($params['date'] ? $params['date'] : $this->date);
347
				$start->setWeekstart();
348
				$this->first = $start->format('ts');
349
				$this->last = $this->bo->date2array($this->first);
350
				$this->last['day'] += ($params['weekend'] == 'true' ? 7 : 5) - 1;
351
				$this->last['hour'] = 23; $this->last['minute'] = $this->last['sec'] = 59;
352
				unset($this->last['raw']);
353
				$this->last = $this->bo->date2ts($this->last);
354
				$this->date_filters['week'] = $label = lang('Week').' '.adodb_date('W',$this->first).': '.$this->bo->long_date($this->first,$this->last);
355
				$search_params['start'] = $this->first;
356
				$search_params['end'] = $this->last;
357
				$params['startdate'] = Api\DateTime::to($this->first, Api\DateTime::ET2);
358
				$params['enddate'] = Api\DateTime::to($this->last, Api\DateTime::ET2);
359
				break;
360
361
			case 'month':
362
			default:
363
				$this->first = $this->bo->date2array($params['date'] ? $params['date'] : $this->date);
364
				$this->first['day'] = 1;
365
				unset($this->first['raw']);
366
				$this->last = $this->first;
367
				$this->last['month'] += 1;
368
				$this->date_filters['month'] = $label = lang(adodb_date('F',$this->bo->date2ts($params['date']))).' '.$this->first['year'];
369
				$this->first = $this->bo->date2ts($this->first);
370
				$this->last = $this->bo->date2ts($this->last);
371
				$this->last--;
372
				$search_params['start'] = $this->first;
373
				$search_params['end'] = $this->last;
374
				$params['startdate'] = Api\DateTime::to($this->first, Api\DateTime::ET2);
375
				$params['enddate'] = Api\DateTime::to($this->last, Api\DateTime::ET2);
376
				break;
377
378
			case 'after':
379
				$this->date = $params['startdate'] ? Api\DateTime::to($params['startdate'],'ts') : $this->date;
380
				$label = lang('After %1',$this->bo->long_date($this->date));
381
				$search_params['start'] = $this->date;
382
				break;
383
		}
384
		if($params['status_filter'])
385
		{
386
			$search_params['filter'] = $params['status_filter'];
387
		}
388
		if ($params['col_filter']['participant'])
389
		{
390
			$search_params['users'] = is_array($params['col_filter']['participant']) ? $params['col_filter']['participant'] : array( $params['col_filter']['participant']);
391
		}
392
		elseif (!$params['col_filter'] || !$params['col_filter']['participant'])
393
		{
394
			$search_params['users'] = $params['owner'] ? $params['owner'] : explode(',',$this->owner);
395
		}
396
		// Allow private to stay for all viewed owners, even if in separate calendars
397
		$search_params['private_allowed'] = (array)$params['selected_owners'] + (array)$search_params['users'];
398
399
		if ($params['col_filter'])
400
		{
401
			$col_filter = array();
402
			foreach($params['col_filter'] as $name => $val)
403
			{
404
				if (!in_array($name, array('participant','row_id')) && (string)$val !== '')
405
				{
406
					$col_filter[$name] = $val;
407
				}
408
			}
409
		}
410
		$rows = $js_integration_data = array();
411
412
		// App header is mostly taken care of on the client side, but here we update
413
		// it to match changing list filters
414
		if($params['view'] && $params['view'] == 'listview' && Api\Json\Response::isJSONResponse())
415
		{
416
			Api\Json\Response::get()->call('app.calendar.set_app_header',
417
				(count($search_params['users']) == 1 ? $this->bo->participant_name($search_params['users'][0]).': ' : '') .
418
				$label);
419
		}
420
		foreach((array) $this->bo->search($search_params, !empty($col_filter) ? $col_filter : null) as $event)
421
		{
422
423
			if ($params['csv_export'])
424
			{
425
				$event['participants'] = implode(",\n",$this->bo->participants($event,true));
426
			}
427
			else
428
			{
429
				$this->to_client($event);
430
			}
431
432
			$matches = null;
433
			if(!(int)$event['id'] && preg_match('/^([a-z_-]+)([0-9]+)$/i',$event['id'],$matches))
434
			{
435
				$app = $matches[1];
436
				$app_id = $matches[2];
437
				$icons = array();
438
				if (($is_private = calendar_bo::integration_get_private($app,$app_id,$event)))
439
				{
440
					$icons[] = Api\Html::image('calendar','private');
441
				}
442
				else
443
				{
444
					$icons = calendar_uiviews::integration_get_icons($app,$app_id,$event);
0 ignored issues
show
Unused Code introduced by
The assignment to $icons is dead and can be removed.
Loading history...
445
				}
446
			}
447
			else
448
			{
449
				$is_private = !$this->bo->check_perms(Acl::READ,$event);
450
			}
451
			if ($is_private)
452
			{
453
				$event['class'] .= 'rowNoView ';
454
			}
455
456
			$event['app'] = 'calendar';
457
			$event['app_id'] = $event['id'];
458
459
			// Edit link
460
			if($app && $app_id)
461
			{
462
				$popup = calendar_uiviews::integration_get_popup($app,$app_id);
463
464
				// Need to strip off 'onclick'
465
				$event['edit_link'] = preg_replace('/ ?onclick="(.+)"/i', '$1', $popup);
466
467
				$event['app'] = $app;
468
				$event['app_id'] = $app_id;
469
470
				// populate js_integration_data, if not already set
471
				if (!isset($js_integration_data[$app]))
472
				{
473
					$js_integration_data[$app] = calendar_bo::integration_get_data($app,'edit_link');
474
				}
475
			}
476
			elseif ($event['recur_type'] != MCAL_RECUR_NONE)
477
			{
478
				$event['app_id'] .= ':'.Api\DateTime::to($event['recur_date'] ? $event['recur_date'] : $event['start'],'ts');
479
			}
480
481
			// Format start and end with timezone
482
			foreach(array('start','end') as $time)
483
			{
484
				$event[$time] = Api\DateTime::to($event[$time],'Y-m-d\TH:i:s\Z');
485
			}
486
487
			$rows[] = $event;
488
			unset($app);
489
			unset($app_id);
490
		}
491
		// set js_calendar_integration object, to use it in app.js cal_open() function
492
		$params['js_integration_data'] = json_encode($js_integration_data);
493
494
		$wv=0;
495
		$dv=0;
496
497
		// Add in some select options
498
		$users = is_array($search_params['users']) ? $search_params['users'] : explode(',',$search_params['users']);
499
500
		$this->bo->warnings['groupmembers'] = '';
501
		if(($message = $this->check_owners_access($users)))
502
		{
503
			Api\Json\Response::get()->error($message);
504
		}
505
		else if($this->bo->warnings['groupmembers'])
506
		{
507
			Api\Json\Response::get()->error($this->bo->warnings['groupmembers']);
508
		}
509
		$rows['sel_options']['filter'] = $this->date_filters;
510
		if($label)
511
		{
512
			$rows['sel_options']['filter'][$params['filter']] = $label;
513
		}
514
		foreach($users as $owner)
515
		{
516
			if(!is_int($owner) && $this->bo->resources[$owner[0]])
517
			{
518
				$app = $this->bo->resources[$owner[0]]['app'];
519
				$_owner = substr($owner,1);
520
				// Try link first
521
				$title = Link::title($app, $_owner );
522
				if($title)
523
				{
524
					$rows['sel_options']['owner'][$owner] = $title;
525
				}
526
			}
527
		}
528
		$params['options-selectcols']['week'] = lang('Week');
529
		$params['options-selectcols']['weekday'] = lang('Weekday');
530
		if ((substr($this->cal_prefs['nextmatch-calendar.list.rows'],0,4) == 'week' && strlen($this->cal_prefs['nextmatch-calendar.list.rows'])==4) || substr($this->cal_prefs['nextmatch-calendar.list.rows'],0,5) == 'week,')
531
		{
532
			$rows['format'] = '32';	// prefix date with week-number
533
			$wv=1;
534
		}
535
		if (!(strpos($this->cal_prefs['nextmatch-calendar.list.rows'],'weekday')===FALSE))
536
		{
537
			$rows['format'] = '16';
538
			$dv=1;
539
		}
540
		if ($wv && $dv)
541
		{
542
			$rows['format'] = '64';
543
		}
544
		if ($this->cat_id) $rows['no_cat_id'] = true;
545
		if (!$GLOBALS['egw_info']['user']['apps']['projectmanager'])
546
		{
547
			$params['options-selectcols']['pm_id'] = false;
548
		}
549
		//_debug_array($rows);
550
		return $this->bo->total;
551
	}
552
553
	/**
554
	 * apply an action to multiple events
555
	 *
556
	 * @param string/int $action 'delete', 'ical', 'print', 'email'
0 ignored issues
show
Documentation Bug introduced by
The doc comment string/int at position 0 could not be parsed: Unknown type name 'string/int' at position 0 in string/int.
Loading history...
557
	 * @param array $checked event id's to use if !$use_all
558
	 * @param boolean $use_all if true use all events of the current selection (in the session)
559
	 * @param int &$success number of succeded actions
560
	 * @param int &$failed number of failed actions (not enought permissions)
561
	 * @param string &$action_msg translated verb for the actions, to be used in a message like %1 events 'deleted'
562
	 * @param string/array $session_name 'calendar_list'
563
	 * @return boolean true if all actions succeded, false otherwise
564
	 */
565
	function action($action,$checked,$use_all,&$success,&$failed,&$action_msg,$session_name,&$msg,$skip_notification=false)
566
	{
567
		//error_log(__METHOD__."('$action', ".array2string($checked).', all='.(int)$use_all.", ...)");
568
		$success = $failed = 0;
569
		$msg = null;
570
571
		// Split out combined values
572
		if(strpos($action, 'status') !== false)
573
		{
574
			list($action, $status) = explode('-', $action);
575
		}
576
		elseif (strpos($action, '_') !== false)
577
		{
578
			list($action, $settings) = explode('_', $action,2);
579
		}
580
581
		if ($use_all)
582
		{
583
			// get the whole selection
584
			$query = is_array($session_name) ? $session_name : Api\Cache::getSession('calendar', $session_name);
585
			@set_time_limit(0);				// switch off the execution time limit, as for big selections it's too small
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for set_time_limit(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

585
			/** @scrutinizer ignore-unhandled */ @set_time_limit(0);				// switch off the execution time limit, as for big selections it's too small

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
586
			$query['num_rows'] = -1;		// all
587
			$readonlys = null;
588
			$this->get_rows($query,$checked,$readonlys,!in_array($action,array('ical','document')));	   // true = only return the id's
0 ignored issues
show
Unused Code introduced by
The call to calendar_uilist::get_rows() has too many arguments starting with ! in_array($action, array('ical', 'document')). ( Ignorable by Annotation )

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

588
			$this->/** @scrutinizer ignore-call */ 
589
          get_rows($query,$checked,$readonlys,!in_array($action,array('ical','document')));	   // true = only return the id's

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...
589
			// Get rid of any extras (rows that aren't events)
590
			if(in_array($action,array('ical','document')))
591
			{
592
				foreach($checked as $key => $event)
593
				{
594
					if(!is_numeric($key))
595
					{
596
						unset($checked[$key]);
597
					}
598
				}
599
			}
600
		}
601
		// for calendar integration we have to fetch all rows and unset the not selected ones, as we can not filter by id
602
		elseif($action == 'document')
603
		{
604
			$query = is_array($session_name) ? $session_name : Api\Cache::getSession('calendar', $session_name);
605
			@set_time_limit(0);				// switch off the execution time limit, as for big selections it's too small
606
			$events = null;
607
			$this->get_rows($query,$events,$readonlys);
608
			foreach($events as $key => $event)
609
			{
610
				$recur_date = Api\DateTime::to($event['recur_date'],'ts');
611
				if (!in_array($event['id'],$checked) && !in_array($event['id'].':'.$recur_date, $checked)) unset($events[$key]);
612
			}
613
			$checked = array_values($events); // Clear keys
614
		}
615
616
		// Actions where one action is done to the group
617
		switch($action)
618
		{
619
			case 'ical':
620
				// compile list of unique cal_id's, as iCal should contain whole series, not recurrences
621
				// calendar_ical->exportVCal needs to read events again, to get them in server-time
622
				$ids = array();
623
				foreach($checked as $id)
624
				{
625
					if (is_array($id)) $id = $id['id'];
626
					// get rid of recurrences, doublicate series and calendar-integration events
627
					if (($id = (int)$id))
628
					{
629
						$ids[$id] = $id;
630
					}
631
				}
632
				$boical = new calendar_ical();
633
				$ical =& $boical->exportVCal($ids, '2.0', 'PUBLISH');
634
				Api\Header\Content::type('event.ics', 'text/calendar', bytes($ical));
635
				echo $ical;
636
				exit();
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
637
638
			case 'document':
639
				if (!$settings) $settings = $GLOBALS['egw_info']['user']['preferences']['calendar']['default_document'];
640
				$document_merge = new calendar_merge();
641
				$msg = $document_merge->download($settings, $checked, '', $GLOBALS['egw_info']['user']['preferences']['calendar']['document_dir']);
642
				$failed = count($checked);
643
				error_log($msg);
644
				return false;
645
		}
646
647
		// Actions where the action is applied to each entry
648
		if(strpos($action, 'timesheet') !== false)
649
		{
650
			$timesheet_bo = new timesheet_bo();
651
		}
652
		foreach($checked as &$id)
653
		{
654
			$recur_date = $app = $app_id = null;
655
			if(is_array($id) && $id['id'])
656
			{
657
				$id = $id['id'];
658
			}
659
			$matches = null;
660
			if(!(int)$id && preg_match('/^([a-z_-]+)([0-9]+)$/i',$id,$matches))
661
			{
662
				$app = $matches[1];
663
				$app_id = $matches[2];
664
				$id = null;
665
			}
666
			else
667
			{
668
				list($id,$recur_date) = explode(':',$id);
669
			}
670
			switch($action)
671
			{
672
				case 'delete':
673
					$action_msg = lang('deleted');
674
					if($settings == 'series')
675
					{
676
						// Delete the whole thing
677
						$recur_date = 0;
678
					}
679
					if ($id && $this->bo->delete($id, $recur_date,false,$skip_notification))
680
					{
681
						$success++;
682
						if(!$recur_date && $settings == 'series')
0 ignored issues
show
Bug Best Practice introduced by
The expression $recur_date 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...
683
						{
684
							// If there are multiple events in a series selected, the next one could purge
685
							foreach($checked as $key => $c_id)
686
							{
687
								list($c_id,$recur_date) = explode(':',$c_id);
688
								if($c_id == $id)
689
								{
690
									unset($checked[$key]);
691
								}
692
							}
693
						}
694
695
						if(Api\Json\Response::isJSONResponse())
696
						{
697
							Api\Json\Response::get()->call('egw.refresh','','calendar',$id,'delete');
698
						}
699
					}
700
					else
701
					{
702
						$failed++;
703
					}
704
					break;
705
				case 'undelete':
706
					$action_msg = lang('recovered');
707
					if($settings == 'series')
708
					{
709
						// unDelete the whole thing
710
						$recur_date = 0;
711
					}
712
					if ($id && ($event = $this->bo->read($id, $recur_date)) && $this->bo->check_perms(Acl::EDIT,$id) &&
713
						is_array($event) && $event['deleted'])
714
					{
715
						$event['deleted'] = null;
716
						if($this->bo->save($event))
717
						{
718
							$success++;
719
720
							if(Api\Json\Response::isJSONResponse())
721
							{
722
								Api\Json\Response::get()->call('egw.dataStoreUID','calendar::'.$id,$this->to_client($this->bo->read($id,$recur_date)));
0 ignored issues
show
Bug introduced by
$this->bo->read($id, $recur_date) cannot be passed to calendar_ui::to_client() as the parameter $event 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

722
								Api\Json\Response::get()->call('egw.dataStoreUID','calendar::'.$id,$this->to_client(/** @scrutinizer ignore-type */ $this->bo->read($id,$recur_date)));
Loading history...
Bug introduced by
Are you sure $id of type void can be used in concatenation? ( Ignorable by Annotation )

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

722
								Api\Json\Response::get()->call('egw.dataStoreUID','calendar::'./** @scrutinizer ignore-type */ $id,$this->to_client($this->bo->read($id,$recur_date)));
Loading history...
723
								Api\Json\Response::get()->call('egw.refresh','','calendar',$id,'edit');
724
							}
725
							break;
726
						}
727
					}
728
					$failed++;
729
					break;
730
				case 'status':
731
					$action_msg = lang('Status changed');
732
					if($id && ($event = $this->bo->read($id, $recur_date)))
733
					{
734
						$old_status = $event['participants'][$GLOBALS['egw_info']['user']['account_id']];
735
						$quantity = $role = null;
736
						calendar_so::split_status($old_status, $quantity, $role);
737
						if ($old_status != $status)
738
						{
739
							//echo "<p>$uid: status changed '$data[old_status]' --> '$status<'/p>\n";
740
							$new_status = calendar_so::combine_status($status, $quantity, $role);
741
							if ($this->bo->set_status($event,$GLOBALS['egw_info']['user']['account_id'],$new_status,$recur_date,
742
								false,true,$skip_notification))
743
							{
744
								if(Api\Json\Response::isJSONResponse())
745
								{
746
									Api\Json\Response::get()->call('egw.dataStoreUID','calendar::'.$id,$this->to_client($this->bo->read($id,$recur_date)));
747
								}
748
								$success++;
749
								//$msg = lang('Status changed');
750
							}
751
							else
752
							{
753
								$failed++;
754
							}
755
						}
756
					}
757
					else
758
					{
759
						$failed++;
760
					}
761
					break;
762
				case 'timesheet-add':
763
					if($id && !$app)
764
					{
765
						$event = $this->bo->read($id, $recur_date);
766
					}
767
					elseif ($app)
768
					{
769
						$query = Api\Cache::getSession('calendar', 'calendar_list');
770
						$query['query'] = $app_id;
771
						$query['search'] = $app_id;
772
						$result = $this->bo->search($query);
773
						$event = $result[$app.$app_id];
774
					}
775
					if(!$event)
776
					{
777
						$failed++;
778
						continue 2;	// +1 for switch
779
					}
780
					$timesheet = array(
781
						'ts_title'		=>	$event['title'],
782
						'ts_description' =>	$event['description'],
783
						'ts_start'		=>	$event['start'],
784
						'ts_duration'	=>	($event['end'] - $event['start']) / 60,
785
						'ts_quantity'	=>	($event['end'] - $event['start']) / 3600,
786
						'ts_owner'		=>	$GLOBALS['egw_info']['user']['account_id'],
787
						'cat_id'		=>	null,
788
						'pl_id'			=>	null
789
					);
790
791
					// Add global categories
792
					$categories = explode(',',$event['category']);
793
					$global_categories = array();
794
					foreach($categories as $cat_id)
795
					{
796
						if($GLOBALS['egw']->categories->is_global($cat_id))
797
						{
798
							$global_categories[] = $cat_id;
799
						}
800
					}
801
					if(count($global_categories))
802
					{
803
						$timesheet['cat_id'] = implode(',', $global_categories);
804
					}
805
					$timesheet_bo->data = array();
806
					$err = $timesheet_bo->save($timesheet);
807
808
					//get the project manager linked to the calnedar entry
809
					$calApp_links = Link::get_links('calendar', $event['id']);
810
					foreach ($calApp_links as $l_app)
811
					{
812
						if ($l_app['app'] == 'projectmanager')
813
						{
814
							$prj_links = $l_app;
815
							//Links timesheet to projectmanager
816
							Link::link('timesheet', $timesheet_bo->data['ts_id'], 'projectmanager', $prj_links['id']);
817
818
						}
819
					}
820
821
					if(!$err)
822
					{
823
						$success++;
824
825
						// Can't link to just one of a recurring series of events
826
						if(!$recur_date || $app) {
827
							// Create link
828
							$link_id = $app ? $app_id : $id;
829
							Link::link($app ? $app : 'calendar', $link_id, 'timesheet', $timesheet_bo->data['ts_id']);
830
						}
831
					}
832
					else
833
					{
834
						$failed++;
835
					}
836
					$msg = lang('Timesheet entries created for ');
837
					break;
838
			}
839
		}
840
		//error_log(__METHOD__."('$action', ".array2string($checked).', '.array2string($use_all).") sucess=$success, failed=$failed, action_msg='$action_msg', msg=".array2string($msg).' returning '.array2string(!$failed));
841
		return !$failed;
842
	}
843
844
	/**
845
	 * Get date ranges to select for merging instead of individual events
846
	 *
847
	 * @param $nm nextmatch array from submit
0 ignored issues
show
Bug introduced by
The type nextmatch 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...
848
	 *
849
	 * @return array of ranges
850
	 */
851
	protected function get_merge_range($nm)
852
	{
853
		$checked = array();
854
		if($nm['filter'] == 'fixed')
855
		{
856
			$checked['start'] = $nm['startdate'];
857
			$last = $this->bo->date2array($nm['enddate']);
858
			$last['hour'] = '23'; $last['minute'] = $last['sec'] = '59';
859
			$checked['end'] = $this->bo->date2ts($last);
860
		}
861
		else
862
		{
863
			switch($nm['filter'])
864
			{
865
				case 'after':
866
					$checked['start'] = $nm['startdate'] ? $nm['startdate'] : strtotime('today');
867
					break;
868
				case 'before':
869
					$checked['end'] = $nm['enddate'] ? $nm['enddate'] : strtotime('tomorrow');
870
					break;
871
				case 'custom':
872
					$checked['start'] = $nm['startdate'];
873
					$checked['end'] = $nm['enddate'];
874
					break;
875
				default:
876
					$date = date_create_from_format('Ymd',$this->date);
877
					$checked['start']= $date->format('U');
878
			}
879
		}
880
		return $checked;
881
	}
882
883
	/**
884
	 * Get actions / context menu items
885
	 *
886
	 * @return array see nextmatch_widget::get_actions()
887
	 */
888
	public function get_actions()
889
	{
890
		$actions = array(
891
			'add' => array(
892
				'caption' => 'Add',
893
				'egw_open' => 'add-calendar',
894
				'hideOnMobile' => true
895
			),
896
			'open' => array(
897
				'caption' => 'Open',
898
				'default' => true,
899
				'allowOnMultiple' => false,
900
				'url' => 'menuaction=calendar.calendar_uiforms.edit&cal_id=$id',
901
				'popup' => Link::get_registry('calendar', 'view_popup'),
902
				'group' => $group=1,
903
				'onExecute' => 'javaScript:app.calendar.cal_open',
904
				'disableClass' => 'rowNoView',
905
			),
906
			'copy' => array(
907
				'caption' => 'Copy',
908
				'group' => $group,
909
				'disableClass' => 'rowNoView',
910
				'url' => 'menuaction=calendar.calendar_uiforms.edit&cal_id=$id&action=copy',
911
				'popup' => Link::get_registry('calendar', 'view_popup'),
912
				'allowOnMultiple' => false,
913
			),
914
			'print' => array(
915
				'caption' => 'Print',
916
				'group' => $group,
917
				'disableClass' => 'rowNoView',
918
				'url' => 'menuaction=calendar.calendar_uiforms.edit&cal_id=$id&print=1',
919
				'popup' => Link::get_registry('calendar', 'view_popup'),
920
				'allowOnMultiple' => false,
921
			),
922
			'select_all' => array(
923
				'caption' => 'Whole query',
924
				'hint' => 'Apply the action on the whole query, NOT only the shown events',
925
				'group' => ++$group,
926
			),
927
			'no_notifications' => array(
928
				'caption' => 'Do not notify',
929
				'checkbox' => true,
930
				'hint' => 'Do not notify of these changes',
931
				'group' => $group,
932
			),
933
		);
934
		$status = array_map('lang',$this->bo->verbose_status);
935
		unset($status['G']);
936
		$actions['status'] = array(
937
			'caption' => 'Change your status',
938
			'icon' => 'check',
939
			'prefix' => 'status-',
940
			'children' => $status,
941
			'group' => ++$group,
942
		);
943
		++$group;	// integration with other apps: infolog, calendar, filemanager
944
		if ($GLOBALS['egw_info']['user']['apps']['filemanager'])
945
		{
946
			$actions['filemanager'] = array(
947
				'icon' => 'filemanager/navbar',
948
				'caption' => 'Filemanager',
949
				'url' => 'menuaction=filemanager.filemanager_ui.index&path=/apps/calendar/$id&ajax=true',
950
				'group' => $group,
951
				'allowOnMultiple' => false,
952
				'onExecute' => 'javaScript:app.calendar.cal_fix_app_id',
953
				'disableClass' => 'rowNoView',
954
			);
955
		}
956
		if ($GLOBALS['egw_info']['user']['apps']['infolog'])
957
		{
958
			$actions['infolog_app'] = array(
959
				'caption' => 'InfoLog',
960
				'icon' => 'infolog/navbar',
961
				'group' => $group,
962
				'allowOnMultiple' => false,
963
				'url' => 'menuaction=infolog.infolog_ui.edit&type=task&action=calendar&action_id=$id',
964
				'popup' => Link::get_registry('infolog', 'add_popup'),
965
			);
966
		}
967
		if($GLOBALS['egw_info']['user']['apps']['mail'])
968
		{
969
			//Send to email
970
			$actions['email'] = array(
971
				'caption' => 'Email',
972
				'icon'	=> 'mail/navbar',
973
				'hideOnDisabled' => true,
974
				'group' => $group,
975
				'allowOnMultiple' => false,
976
				'children' => array(
977
					'mail' => array(
978
						'caption' => 'Mail all participants',
979
						'onExecute' => 'javaScript:app.calendar.action_mail',
980
981
					),
982
					'sendrequest' => array(
983
						'caption' => 'Meetingrequest to all participants',
984
						'onExecute' => 'javaScript:app.calendar.action_mail',
985
					)
986
				),
987
			);
988
		}
989
990
		if ($GLOBALS['egw_info']['user']['apps']['timesheet'])
991
		{
992
			$actions['timesheet'] = array(	// interactive add for a single event
993
				'icon' => 'timesheet/navbar',
994
				'caption' => 'Timesheet',
995
				'group' => $group,
996
				'allowOnMultiple' => false,
997
				'hideOnDisabled' => true,	// show only one timesheet action in context menu
998
				'onExecute' => 'javaScript:app.calendar.action_open',
999
				'open' => '{"app": "timesheet", "type": "add", "extra": "link_app[]=$app&link_id[]=$app_id"}',
1000
				'popup' => Link::get_registry('timesheet', 'add_popup'),
1001
			);
1002
			$actions['timesheet-add'] = array(	// automatic add for multiple events
1003
				'icon' => 'timesheet/navbar',
1004
				'caption' => 'Timesheet',
1005
				'group' => $group,
1006
				'allowOnMultiple' => 'only',
1007
				'hideOnDisabled' => true,	// show only one timesheet action in context menu
1008
			);
1009
		}
1010
		$actions['ical'] = array(
1011
			'icon' => 'ical',
1012
			'caption' => 'Export iCal',
1013
			'group' => ++$group,
1014
			'hint' => 'Download this event as iCal',
1015
			'disableClass' => 'rowNoView',
1016
			'postSubmit' => true,	// download needs post submit (not Ajax) to work
1017
		);
1018
		$actions['documents'] = calendar_merge::document_action(
1019
			$this->bo->cal_prefs['document_dir'], ++$group, 'Insert in document', 'document_',
1020
			$this->bo->cal_prefs['default_document'],Api\Storage\Merge::getExportLimit('calendar')
1021
		);
1022
		++$group;
1023
		$actions['delete'] = array(
1024
			'caption' => 'Delete',
1025
			'onExecute' => 'javaScript:app.calendar.cal_delete',
1026
			'group' => $group,
1027
			'disableClass' => 'rowNoDelete',
1028
		);
1029
		// Add in deleted for admins
1030
		if($GLOBALS['egw_info']['server']['calendar_delete_history'])
1031
		{
1032
			$actions['undelete'] = array(
1033
				'caption' => 'Un-delete',
1034
				'onExecute' => 'javaScript:app.calendar.cal_delete',
1035
				'icon' => 'revert',
1036
				'hint' => 'Recover this event',
1037
				'group' => $group,
1038
				'enableClass' => 'rowDeleted',
1039
				'hideOnDisabled' => true,
1040
			);
1041
		}
1042
1043
		//_debug_array($actions);
1044
		return $actions;
1045
	}
1046
}
1047