calendar_uilist::adjust_for_search()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 8
nc 2
nop 2
dl 0
loc 13
rs 10
c 0
b 0
f 0
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