1<?php
2/**
3 * Tracker - Universal tracker (bugs, feature requests, ...) with voting and bounties
4 *
5 * @link http://www.egroupware.org
6 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
7 * @package tracker
8 * @copyright (c) 2006-16 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
9 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
10 * @version $Id$
11 */
12
13use EGroupware\Api;
14use EGroupware\Api\Link;
15use EGroupware\Api\Framework;
16use EGroupware\Api\Egw;
17use EGroupware\Api\Acl;
18use EGroupware\Api\Etemplate;
19
20/**
21 * User Interface of the tracker
22 */
23class tracker_ui extends tracker_bo
24{
25	/**
26	 * Functions callable via menuaction
27	 *
28	 * @var array
29	 */
30	var $public_functions = array(
31		'edit'  => true,
32		'index' => true,
33		'tprint'=> true,
34		'mail_import' => True,
35	);
36	/**
37	 * Displayed instead of the '@' in email-addresses
38	 *
39	 * @var string
40	 */
41	var $mangle_at = ' -at- ';
42	/**
43	 * reference to the preferences of the user
44	 *
45	 * @var array
46	 */
47	var $prefs;
48
49	/**
50	 * allowed units and hours per day, can be overwritten by the projectmanager configuration, default all units, 8h
51	 *
52	 * @var string
53	 */
54	var $duration_format = ',';	// comma is necessary!
55
56	/**
57	 * Etemplate used for rendering
58	 *
59	 * @var Etemplate
60	 */
61	public $template;
62
63	/**
64	 * Constructor
65	 *
66	 * @return tracker_ui
67	 */
68	function __construct()
69	{
70		parent::__construct();
71		$this->prefs =& $GLOBALS['egw_info']['user']['preferences']['tracker'];
72
73		// read the duration format from project-manager
74		if ($GLOBALS['egw_info']['apps']['projectmanager'])
75		{
76			$pm_config = Api\Config::read('projectmanager');
77			$this->duration_format = str_replace(',','',implode('', (array)$pm_config['duration_units'])).','.$pm_config['hours_per_workday'];
78			unset($pm_config);
79		}
80	}
81
82	/**
83	 * Print a tracker item
84	 *
85	 * @return string html-content, if sitemgr otherwise null
86	 */
87	function tprint()
88	{
89		// Check if exists
90		if ((int)$_GET['tr_id'])
91		{
92			if (!$this->read($_GET['tr_id']))
93			{
94				return lang('Tracker item not found !!!');
95			}
96		}
97		else	// new item
98		{
99			return lang('Tracker item not found !!!');
100		}
101		if (!is_object($this->tracking))
102		{
103			$this->tracking = new tracker_tracking($this);
104		}
105
106		if ($this->data['tr_edit_mode'] == 'html')
107		{
108			$this->tracking->html_content_allow = true;
109		}
110
111		$details = $this->tracking->get_body(true,$this->data,$this->data);
112		if (!$details)
113		{
114			return implode(', ',$this->tracking->errors);
115		}
116		$GLOBALS['egw']->framework->render($details,'',false);
117	}
118
119	/**
120	 * Edit a tracker item in a popup
121	 *
122	 * @param array $content =null eTemplate content
123	 * @param string $msg =''
124	 * @param boolean $popup =true use or not use a popup
125	 * @return string html-content, if sitemgr otherwise null
126	 */
127	function edit($content=null,$msg='',$popup=true)
128	{
129		if ($this->htmledit || (isset($content['tr_edit_mode']) && $content['tr_edit_mode']=='html'))
130		{
131			$tr_editor_mode = 'html';
132		}
133		else
134		{
135			$tr_editor_mode = 'ascii';
136		}
137
138		//_debug_array($content);
139		if (!is_array($content))
140		{
141			if ($_GET['msg']) $msg = strip_tags($_GET['msg']);
142
143			// edit or new?
144			if ((int)$_GET['tr_id'])
145			{
146				$own_referer = Api\Header\Referer::get();
147				if (!$this->read($_GET['tr_id']))
148				{
149					Framework::window_close(lang('Tracker item not found !!!'));
150				}
151				else
152				{
153					// Set the ticket as seen by this user
154					self::seen($this->data, true);
155
156					// editing, preventing/fixing mixed ascii-html
157					if ($this->data['tr_edit_mode'] == 'ascii' && $this->htmledit)
158					{
159						// non html items edited by html (add nl2br)
160						$tr_editor_mode = 'ascii';
161					}
162					if ($this->data['tr_edit_mode'] == 'html' && !$this->htmledit)
163					{
164						// html items edited in ascii mode (prevent changing to html)
165						$tr_editor_mode = 'html';
166					}
167					//echo "<p>data[tr_edit_mode]={$this->data['tr_edit_mode']}, this->htmledit=".array2string($this->htmledit)."</p>\n";
168					// Ascii Replies are converted to html, if htmledit is disabled (default), we allways convert, as this detection is weak
169					// Conversion must be based on ticket setting, since it persists after the config setting is changed
170					foreach ($this->data['replies'] as &$reply)
171					{
172						if (!($this->data['tr_edit_mode'] == 'html')|| (strlen($reply['reply_message'])==strlen(strip_tags($reply['reply_message'])))) //(stripos($reply['reply_message'], '<br') === false && stripos($reply['reply_message'], '<p>') === false))
173						{
174							$reply['reply_message'] = Api\Html::htmlspecialchars($reply['reply_message']);
175						}
176					}
177					//// Make sure add comment file directory is empty, in case someone closed
178					// it without saving after selecting or uploading a file
179					if($this->file_access($tr_id, Acl::DELETE))
180					{
181						$this->remove_comment_dir($tr_id);
182					}
183				}
184				$needInit = false;
185			}
186			else	// new item
187			{
188				$needInit = true;
189				$regardInInit = array();
190			}
191			// for new items we use the session-state or $_GET['tracker']
192			if (!$this->data['tr_id'])
193			{
194				$regardInInit = array(
195					'tr_tracker' => $this->data['tr_tracker']
196				);
197				if (($state = Api\Cache::getSession('tracker','index'.
198					(isset($this->trackers[(int)$_GET['only_tracker']]) ? '-'.$_GET['only_tracker'] : ''))))
199				{
200					$this->data['tr_tracker'] = $regardInInit['tr_tracker'] = $state['col_filter']['tr_tracker'] ? $state['col_filter']['tr_tracker'] : $this->data['tr_tracker'];
201					$this->data['cat_id']     = $regardInInit['cat_id'] = $state['cat_id'] ? $state['cat_id'] : false;
202					$this->data['tr_version'] = $regardInInit['tr_version'] = $state['filter2'] ? $state['filter2'] : $GLOBALS['egw_info']['user']['preferences']['tracker']['default_version'];
203				}
204				if (isset($this->trackers[(int)$_GET['tracker']]))
205				{
206					$this->data['tr_tracker'] = $regardInInit['tr_tracker'] = (int)$_GET['tracker'];
207				}
208				// State can have more than one tracker selected, edit has only 1
209				if(is_array($this->data['tr_tracker']))
210				{
211					$this->data['tr_tracker'] = $regardInInit['tr_tracker']  = (int)  array_pop($this->data['tr_tracker']);
212				}
213			}
214
215
216			// Copy
217			if($_GET['tr_id'] && $_GET['makecp'])
218			{
219				$this->copy($this->data);
220			}
221			// initialize and try to merge what we already have
222			if ($needInit)
223			{
224				$this->init($regardInInit);
225			}
226			if ($_GET['no_popup'] || $_GET['nopopup']) $popup = false;
227
228			// check if user has rights to create new entries and fail if not
229			if (!$this->data['tr_id'] && !$this->check_rights($this->field_acl['add'],null,null,null,'add'))
230			{
231				$msg = lang('Permission denied !!!');
232				if ($popup)
233				{
234					$GLOBALS['egw']->framework->render('<h1 style="color: red;">'.$msg."</h1>\n",null,true);
235					exit();
236				}
237				else
238				{
239					unset($_GET['tr_id']);	// in case it's still set
240					return $this->index(null,$this->data['tr_tracker'],$msg);
241				}
242			}
243			// on resticted trackers, check if the user has read access, OvE, 20071012
244			$restrict = false;
245			if($this->data['tr_id'])
246			{
247				if (!$this->is_staff($this->data['tr_tracker']) &&	// user has to be staff or
248					!array_intersect($this->data['tr_assigned'],	// he or a group he is a member of is assigned
249						array_merge((array)$this->user,$GLOBALS['egw']->accounts->memberships($this->user,true))))
250				{
251					// if we have group OR creator restrictions
252					if ($this->restrictions[$this->data['tr_tracker']]['creator'] ||
253						$this->restrictions[$this->data['tr_tracker']]['group'])
254					{
255						// we need to be creator OR group member
256						if (!($this->restrictions[$this->data['tr_tracker']]['creator'] &&
257								$this->data['tr_creator'] == $this->user ||
258							$this->restrictions[$this->data['tr_tracker']]['group'] &&
259								in_array($this->data['tr_group'], $GLOBALS['egw']->accounts->memberships($this->user,true))))
260						{
261							$restrict = true;	// if not --> no access
262						}
263					}
264					// Check queue access if enabled and that no has access to queue 0 (All)
265					if ($this->enabled_queue_acl_access && !$this->trackers[$this->data['tr_tracker']] && !$this->is_user(0,$this->user))
266					{
267						$restrict = true;
268					}
269					// Check for specific access
270					if($GLOBALS['egw']->acl->check('A'.$this->data['tr_id'], Acl::READ, 'tracker'))
271					{
272						$restrict = false;
273					}
274				}
275			}
276			if ($restrict)
277			{
278				$msg = lang('Permission denied !!!');
279				if ($popup)
280				{
281					$GLOBALS['egw']->framework->render('<h1 style="color: red;">'.$msg."</h1>\n",null,false);
282					exit();
283				}
284				else
285				{
286					unset($_GET['tr_id']);	// in case it's still set
287					return $this->index(null,$this->data['tr_tracker'],$msg);
288				}
289			}
290		}
291		else	// submitted form
292		{
293			//_debug_array($content);
294			$button = @key($content['button']); unset($content['button']);
295			if ($content['bounties']['bounty']) $button = 'bounty'; unset($content['bounties']['bounty']);
296			$popup = $content['popup']; unset($content['popup']);
297			$own_referer = $content['own_referer']; unset($content['own_referer']);
298
299			$this->data = $content;
300			unset($this->data['bounties']['new']);
301			switch($button)
302			{
303				case 'save':
304				case 'apply':
305					if (is_array($this->data['tr_cc']))
306					{
307						foreach($this->data['tr_cc'] as $i => $value)
308						{
309							//imap_rfc822 should not be used, but it works reliable here, until we have some regex solution or use horde stuff
310							$addresses = imap_rfc822_parse_adrlist($value, '');
311							//error_log(__METHOD__.__LINE__.$value.'->'.array2string($addresses[0]));
312							$this->data['tr_cc'][$i]=$addresses[0]->host ? $addresses[0]->mailbox.'@'.$addresses[0]->host : $addresses[0]->mailbox;
313						}
314						$this->data['tr_cc'] = implode(',',$this->data['tr_cc']);
315					}
316					if (!$this->data['tr_id'] && !$this->check_rights($this->field_acl['add'],null,null,null,'add'))
317					{
318						$msg = lang('Permission denied !!!');
319						break;
320					}
321
322					$readonlys = $this->readonlys_from_acl();
323
324					// Save Current edition mode preventing mixed types
325					if ($this->data['tr_edit_mode'] == 'html' && !$this->htmledit)
326					{
327						$this->data['tr_edit_mode'] = 'html';
328					}
329					elseif ($this->data['tr_edit_mode'] == 'ascii' && $this->htmledit)
330					{
331						$this->data['tr_edit_mode'] = 'ascii';
332					}
333					else
334					{
335						$this->htmledit ? $this->data['tr_edit_mode'] = 'html' : $this->data['tr_edit_mode'] = 'ascii';
336					}
337
338					if ($this->htmledit && $this->data['tr_id'] && is_array($content['link_to']['to_id']))
339					{
340						mail_integration::fix_inline_images('tracker', $this->data['tr_id'], $content['link_to']['to_id'], $content['reply_message']);
341						$this->data['reply_message'] = $content['reply_message'];
342					}
343
344					$ret = $this->save();
345
346					$this->comment_files($this->data['tr_id'],
347						$this->data['replies'][0]['reply_id'],
348						$this->data
349					);
350
351					if ($ret === false)
352					{
353						$msg = lang('Nothing to save.');
354						$state = Api\Cache::getSession('tracker', 'index');
355						Framework::refresh_opener($msg,'tracker',$this->data['tr_id'],'edit');
356
357						// only change to current tracker, if not all trackers displayed
358						($state['col_filter']['tr_tracker'] ? '&tracker='.$this->data['tr_tracker'] : '')."';";
359					}
360					elseif ($ret === 'tr_modifier' || $ret === 'tr_modified')
361					{
362						$msg .= ($msg ? ', ' : '') .lang('Error: the entry has been updated since you opened it for editing!').'<br />'.
363							lang('Copy your changes to the clipboard, %1reload the entry%2 and merge them.','<a href="'.
364								htmlspecialchars(Egw::link('/index.php',array(
365									'menuaction' => 'tracker.tracker_ui.edit',
366									'tr_id'    => $this->data['tr_id'],
367									//'referer'    => $referer,
368								))).'">','</a>');
369						break;
370					}
371					elseif ($ret == 0 && !is_string($ret))
372					{
373						$msg = lang('Entry saved');
374						//apply defaultlinks
375						usort($this->all_cats,function($a, $b)
376						{
377							return strcasecmp($a['name'], $b['name']);
378						});
379						foreach($this->all_cats as $cat)
380						{
381							if (!is_array($data = $cat['data'])) $data = array('type' => $data);
382							//echo "<p>".$this->data['tr_tracker'].": $cat[name] ($cat[id]/$cat[parent]/$cat[main]): ".print_r($data,true)."</p>\n";
383
384							if ($cat['parent'] == $this->data['tr_tracker'] && $data['type'] != 'tracker' && $data['type']=='project')
385							{
386								if (!Link::get_link('tracker',$this->data['tr_id'],'projectmanager',$data['projectlist']))
387								{
388									Link::link('tracker',$this->data['tr_id'],'projectmanager',$data['projectlist']);
389								}
390							}
391						}
392						if (is_array($content['link_to']['to_id']) && count($content['link_to']['to_id']))
393						{
394							Link::link('tracker',$this->data['tr_id'],$content['link_to']['to_id']);
395
396							// Check if we have inline images from mail
397							if($this->htmledit && mail_integration::fix_inline_images('tracker', $this->data['tr_id'],
398									$content['link_to']['to_id'], $content['tr_description']))
399							{
400								$this->update(array(
401									'tr_description' => $content['tr_description'],
402								));
403							}
404
405							// check if we have dragged in images and fix their image urls
406							if (Etemplate\Widget\Vfs::fix_html_dragins('tracker', $this->data['tr_id'],
407								$content['link_to']['to_id'], $content['tr_description']))
408							{
409								$this->update(array(
410									'tr_description' => $content['tr_description'],
411								));
412							}
413						}
414						$state = Api\Cache::getSession('tracker', 'index');
415						Framework::refresh_opener($msg, 'tracker',$this->data['tr_id'],'edit');
416					}
417					else
418					{
419						$msg = lang('Error saving the entry!!!') . "\n" . lang($ret);
420						break;
421					}
422					if ($button == 'apply')
423					{
424						$_GET['tr_id'] = $this->data['tr_id'];
425						return $this->edit($_GET['tr_id'], $msg, $popup);
426					}
427					// fall-through for save
428				case 'cancel':
429					if ($popup)
430					{
431						Framework::window_close();
432						exit();
433					}
434					unset($_GET['tr_id']);	// in case it's still set
435					if($own_referer && strpos($own_referer,'cd=yes') === false &&
436						strpos($own_referer,'tr_id='.$this->data['tr_id']) === FALSE)
437					{
438						// Go back to where you came from
439						Egw::redirect_link($own_referer);
440					}
441					if (Api\Json\Response::isJSONResponse())
442					{
443						Api\Json\Response::get()->call('egw.open_link','tracker.tracker_ui.index&ajax=true','_self',false,'tracker');
444						return;
445					}
446					return $this->index(null,$this->data['tr_tracker'],$msg);
447
448				case 'vote':
449					if ($this->cast_vote())
450					{
451						$msg = lang('Thank you for voting.');
452						if ($popup)
453						{
454							Framework::refresh_opener($msg, 'tracker',$this->data['tr_id'], 'edit');
455						}
456					}
457					break;
458
459				case 'bounty':
460					if (!$this->allow_bounties) break;
461					$bounty = $content['bounties']['new'];
462					if (!$this->is_anonymous())
463					{
464						if (!$bounty['bounty_name']) $bounty['bounty_name'] = $GLOBALS['egw_info']['user']['account_fullname'];
465						if (!$bounty['bounty_email']) $bounty['bounty_email'] = $GLOBALS['egw_info']['user']['account_email'];
466					}
467					if (!$bounty['bounty_amount'] || !$bounty['bounty_name'] || !$bounty['bounty_email'])
468					{
469						$msg = lang('You need to specify amount, donators name AND email address!');
470					}
471					elseif ($this->save_bounty($bounty))
472					{
473						$msg = lang('Thank you for setting this bounty.').
474							' '.lang('The bounty will NOT be shown, until the money is received.');
475						array_unshift($this->data['bounties'],$bounty);
476						unset($content['bounties']['new']);
477					}
478					break;
479
480				default:
481					if (!$this->allow_bounties) break;
482					// check delete bounty
483					$id = @key($this->data['bounties']['delete']);
484					if ($id)
485					{
486						unset($this->data['bounties']['delete']);
487						if ($this->delete_bounty($id))
488						{
489							$msg = lang('Bounty deleted');
490							foreach($this->data['bounties'] as $n => $bounty)
491							{
492								if ($bounty['bounty_id'] == $id)
493								{
494									unset($this->data['bounties'][$n]);
495									break;
496								}
497							}
498						}
499						else
500						{
501							$msg = lang('Permission denied !!!');
502						}
503					}
504					else
505					{
506						// check confirm bounty
507						$id = @key($this->data['bounties']['confirm']);
508						if ($id)
509						{
510							unset($this->data['bounties']['confirm']);
511							foreach($this->data['bounties'] as $n => $bounty)
512							{
513								if ($bounty['bounty_id'] == $id)
514								{
515									if ($this->save_bounty($this->data['bounties'][$n]))
516									{
517										$msg = lang('Bounty confirmed');
518										Framework::refresh_opener($msg, 'tracker',$this->data['tr_id'], 'edit');
519									}
520									else
521									{
522										$msg = lang('Permission denied !!!');
523									}
524									break;
525								}
526							}
527						}
528					}
529					break;
530			}
531		}
532		$tr_id = $this->data['tr_id'];
533		if (!($tracker = $this->data['tr_tracker']))
534		{
535			reset($this->trackers);
536			$tracker = @key($this->trackers);
537		}
538		if (!$readonlys) $readonlys = $this->readonlys_from_acl();
539
540		$preserv = $content = $this->data;
541		$content['id'] = $tr_id;
542		if ($content['tr_edit_mode'] == 'ascii' && $content['tr_description'] && $readonlys['tr_description'])
543		{
544			// non html view in a readonly htmlarea (div) needs nl2br
545			$content['tr_description'] = htmlspecialchars($content['tr_description']);
546			$tr_editor_mode = 'ascii';
547		}
548
549		if ($this->allow_bounties)
550		{
551			if (is_array($content['bounties']))
552			{
553				$total = 0;
554				foreach($content['bounties'] as $bounty)
555				{
556					$total += $bounty['bounty_amount'];
557					// confirmed bounties cant be deleted and need no confirm button
558					$readonlys['delete['.$bounty['bounty_id'].']'] =
559						$readonlys['confirm['.$bounty['bounty_id'].']'] = !$this->is_admin($tracker) || $bounty['bounty_confirmed'];
560				}
561				$content['bounties']['num_bounties'] = count($content['bounties']);
562				array_unshift($content['bounties'],false);	// we need the array index to start with 2!
563				array_unshift($content['bounties'],false);
564				$content['bounties']['total'] = $total ? sprintf('%4.2lf',$total) : '';
565			}
566			$content['bounties']['currency'] = $this->currency;
567			$content['bounties']['is_admin'] = $this->is_admin($tracker);
568		}
569		$statis = $this->get_tracker_stati($tracker);
570		$content += array(
571			'msg' => $msg,
572			'tr_description_mode'    => $readonlys['tr_description'],
573			'on_cancel' => $popup ? 'egw(window).close();' : 'egw.open_link("tracker.tracker_ui.index&ajax=true","_self",false,"tracker")',
574			'no_vote' => '',
575			'show_dates' => $this->show_dates,
576			'link_to' => array(
577				'to_id' => $tr_id,
578				'to_app' => 'tracker',
579			),
580			'status_help' => !$this->pending_close_days ? lang('Pending items never get close automatic.') :
581				lang('Pending items will be closed automatic after %1 days without response.',$this->pending_close_days),
582			'history' => array(
583				'id'  => $tr_id,
584				'app' => 'tracker',
585				'status-widgets' => array(
586					'Co' => 'select-percent',
587					'St' => &$statis,
588					'Ca' => 'select-cat',
589					'Tr' => 'select-cat',
590					'Ve' => 'select-cat',
591					'As' => 'select-account',
592					'Cr' => 'select-account',
593					'pr' => array('Public','Private'),
594					'Cl' => 'date-time',
595					'tr_startdate' => 'date-time',
596					'tr_duedate' => 'date-time',
597					'Re' => self::$resolutions + $this->get_tracker_labels('resolution',$tracker),
598					'Gr' => 'select-account',
599					'comment' => array('label','date-time','diff'),
600				),
601			),
602		);
603		if ($this->allow_bounties && !$this->is_anonymous())
604		{
605			$content['bounties']['user_name'] = $GLOBALS['egw_info']['user']['account_fullname'];
606			$content['bounties']['user_email'] = $GLOBALS['egw_info']['user']['account_email'];
607		}
608		$preserv['popup'] = $popup;
609		$preserv['own_referer'] = $own_referer;
610
611		if (!$tr_id && isset($_REQUEST['link_app']) && isset($_REQUEST['link_id']) && !is_array($content['link_to']['to_id']))
612		{
613			$link_ids = is_array($_REQUEST['link_id']) ? $_REQUEST['link_id'] : array($_REQUEST['link_id']);
614			foreach(is_array($_REQUEST['link_app']) ? $_REQUEST['link_app'] : array($_REQUEST['link_app']) as $n => $link_app)
615			{
616				$link_id = $link_ids[$n];
617				if (preg_match('/^[a-z_0-9-]+:[:a-z_0-9-]+$/i',$link_app.':'.$link_id))	// gard against XSS
618				{
619					switch($link_app)
620					{
621						case 'infolog':
622							static $infolog_bo=null;
623							if(!$infolog_bo) $infolog_bo = new infolog_bo();
624							$infolog = $app_entry = $infolog_bo->read($link_id);
625							$content = array_merge($content, array(
626								'tr_owner'	=> $infolog['info_owner'],
627								'tr_private'	=> $infolog['info_access'] == 'private',
628								'tr_summary'	=> $infolog['info_subject'],
629								'tr_description'	=> $infolog['info_des'],
630								'tr_cc'		=> $infolog['info_cc'],
631								'tr_created'	=> $infolog['info_startdate']
632							));
633
634							// Categories are different, no globals.  Match by name.
635							$match = array(
636								$infolog_bo->enums['type'][$infolog['info_type']] => array(
637									'field'	=> 'tr_tracker',
638									'source'=> $this->trackers
639								),
640								Api\Categories::id2name($infolog['info_cat']) => array(
641									'field'	=> 'cat_id',
642									'source'=> $this->get_tracker_labels('cat',$tracker)
643								)
644							);
645							foreach($match as $info_field => $info)
646							{
647								$content[$info['field']] = array_search($info_field,$info['source']);
648							}
649
650							// Try to match priorities
651							foreach($this->get_tracker_priorities($content['tr_tracker'], $content['cat_id']) as $p => $label)
652							{
653								if(stripos($label, $infolog_bo->enums['priority'][$infolog['info_priority']]) !== false)
654								{
655									$content['tr_priority'] = $p;
656									break;
657								}
658							}
659
660							// Add responsible as participant - filtered later
661							foreach($infolog['info_responsible'] as $responsible) {
662								$content['tr_assigned'][] = $responsible;
663							}
664
665							// Copy infolog's links
666							foreach(Link::get_links('infolog',$link_id) as $copy_link)
667							{
668								Link::link('tracker', $content['link_to']['to_id'], $copy_link['app'], $copy_link['id'],$copy_link['remark']);
669							}
670							break;
671
672					}
673					// Copy same custom fields
674					$_cfs = Api\Storage\Customfields::get('tracker');
675					$link_app_cfs = Api\Storage\Customfields::get($link_app);
676					foreach($_cfs as $name => $settings)
677					{
678						unset($settings);
679						if($link_app_cfs[$name]) $content['#'.$name] = $app_entry['#'.$name];
680					}
681					Link::link('tracker',$content['link_to']['to_id'],$link_app,$link_id);
682				}
683			}
684		}
685		// options for creator selectbox (allways add current selected user!)
686		if ($readonlys['tr_creator'])
687		{
688			$creators = array();
689		}
690		else
691		{
692			$creators = $this->get_staff($tracker,0,'usersANDtechnicians');
693		}
694		if ($content['tr_creator'] && !isset($creators[$content['tr_creator']]))
695		{
696			$creators[$content['tr_creator']] = Api\Accounts::username($content['tr_creator']);
697		}
698
699
700		$account_select_pref = $GLOBALS['egw_info']['user']['preferences']['common']['account_selection'];
701		$sel_options = array(
702			'tr_tracker'  => &$this->trackers,
703			'cat_id'      => $this->get_tracker_labels('cat',is_array($tracker) && count($tracker) == 1?$tracker[0]:$tracker, $default_category),
704			'tr_version'  => $this->get_tracker_labels('version',$tracker),
705			'tr_priority' => $this->get_tracker_priorities($tracker,$content['cat_id'], true, $default_priority),
706			'tr_status'   => &$statis,
707			'tr_resolution' => $this->get_tracker_labels('resolution',$tracker),
708			'tr_assigned' => $account_select_pref == 'none' ? array() : $this->get_staff($tracker,$this->allow_assign_groups,$this->allow_assign_users?'usersANDtechnicians':'technicians'),
709			'tr_creator'  => $creators,
710			// New items default to primary group is no right to change the group
711			'tr_group' => $account_select_pref == 'none' ? array() : $this->get_groups(!$this->check_rights($this->field_acl['tr_group'],$tracker,null,null,'tr_group') && !$this->data['tr_id']),
712			'canned_response' => $this->get_tracker_labels('response'),
713		);
714
715		// Keep updating category & priority to default until it's saved
716		if(!$tr_id)
717		{
718 			$content['cat_id'] = $regardInInit['cat_id'] ? $regardInInit['cat_id'] : ($default_category ? (int)$default_category : $this->data['cat_id']);
719			$content['tr_priority'] = $default_priority ? (int)$this->data['tr_priority'] : $this->data['tr_priority'];
720		}
721
722		foreach($this->field2history as $field => $status)
723		{
724			$sel_options['status'][$status] = $this->field2label[$field];
725		}
726		$sel_options['status']['xb'] = 'Bounty deleted';
727		$sel_options['status']['bo'] = 'Bounty set';
728		$sel_options['status']['Bo'] = 'Bounty confirmed';
729		$sel_options['status']['comment'] = 'Comment';
730
731		$readonlys['tabs'] = array(
732			'comments' => !$tr_id || !$content['num_replies'],
733			'add_comment' => !$tr_id || $readonlys['reply_message'],
734			'history'  => !$tr_id,
735			'bounties' => !$this->allow_bounties,
736			'custom'   => !Api\Storage\Customfields::get('tracker', false, $content['tr_tracker']),
737		);
738		// Make link_to readonly if the user has no EDIT access
739		$readonlys['link_to'] = !$this->file_access($tr_id, Acl::EDIT);
740
741		if ($tr_id && $readonlys['reply_message'])
742		{
743			$readonlys['button[save]'] = true;
744		}
745		if (!$tr_id && $readonlys['add'])
746		{
747			$msg = lang('Permission denied !!!');
748			$readonlys['button[save]'] = true;
749		}
750		// Assigned & group are not select-account widgets, so we need to apply
751		// none preference (no value, no options) here.
752		if($account_select_pref == 'none')
753		{
754			$readonlys['tr_assigned'] = true;
755			$readonlys['tr_group'] = true;
756		}
757		if (!$this->allow_voting || !$tr_id || $readonlys['vote'] || ($voted = $this->check_vote()))
758		{
759			$readonlys['button[vote]'] = true;
760			if ($tr_id && $this->allow_voting)
761			{
762				$content['no_vote'] = is_int($voted) ? lang('You voted %1.',
763					date($GLOBALS['egw_info']['user']['preferences']['common']['dateformat'].
764					($GLOBALS['egw_info']['user']['preferences']['common']['timeformat']==12?' h:i a':' H:i'),$voted)) :
765					lang('You need to login to vote!');
766			}
767		}
768		if ($readonlys['canned_response'])
769		{
770			$content['no_canned'] = true;
771		}
772		$content['no_links'] = $readonlys['link_to'];
773		$content['bounties']['no_set_bounties'] = $readonlys['bounty'];
774		//error_log(__METHOD__.__LINE__.':'.is_array($tracker)?$tracker[0]:$tracker);
775		$what = ($tracker && isset($this->trackers[(is_array($tracker)?$tracker[0]:$tracker)]) ? $this->trackers[(is_array($tracker)?$tracker[0]:$tracker)] : lang('Tracker'));
776		$GLOBALS['egw_info']['flags']['app_header'] = $tr_id ? lang('Edit %1',$what) : lang('New %1',$what);
777
778		$tpl = $this->template ? $this->template : new Etemplate();
779		$tpl->read('tracker.edit');
780		// use a type-specific template (tracker.edit.xyz), if one exists, otherwise fall back to the generic one
781		if (!$tpl->read('tracker.edit'.(isset($this->trackers[(is_array($tracker)?$tracker[0]:$tracker)])?'.'.trim($this->trackers[(is_array($tracker)?$tracker[0]:$tracker)]):'')))
782		{
783			$tpl->read('tracker.edit');
784		}
785
786		if ($this->tracker_has_cat_specific_priorities($tracker))
787		{
788			$tpl->set_cell_attribute('cat_id','onchange','widget.getInstanceManager().submit(null,false,true); return false;');
789		}
790		// No notifications needs label hidden too
791		if($readonlys['no_notifications'])
792		{
793			$tpl->set_cell_attribute('no_notifications', 'disabled', true);
794		}
795
796		if ($content['tr_assigned'] && !is_array($content['tr_assigned']))
797		{
798			$content['tr_assigned'] = explode(',',$content['tr_assigned']);
799		}
800		if (is_array($content['tr_assigned']) && count($content['tr_assigned']) > 1)
801		{
802			$tpl->set_cell_attribute('tr_assigned','size','3+');
803		}
804		$tpl->set_cell_attribute('tr_description', 'mode', $tr_editor_mode);
805		$tpl->set_cell_attribute('reply_message', 'mode',$tr_editor_mode);
806
807		$this->setup_comments($tpl, $content, $preserv);
808
809		if (!empty($content['tr_cc'])&&!is_array($content['tr_cc']))$content['tr_cc'] = explode(',',$content['tr_cc']);
810		return $tpl->exec('tracker.tracker_ui.edit',$content,$sel_options,$readonlys,$preserv,$popup ? 2 : 0);
811	}
812
813	/**
814	 * Set up the template / content for editable comments
815	 *
816	 * Editable widgets, context menu actions
817	 */
818	protected function setup_comments(Etemplate &$tpl, Array &$content, Array &$preserve)
819	{
820		// Comment visibility
821		if (is_array($content['replies']))
822		{
823			foreach($content['replies'] as $key => &$reply)
824			{
825				if(!$reply)
826				{
827					unset($content['replies'][$key]);
828					continue;
829				}
830				if (isset($content['replies'][$key]['reply_visible'])) {
831					$reply['reply_visible_class'] = 'reply_visible_'.$reply['reply_visible'];
832					if($this->check_rights($this->field_acl['edit_reply'], null, null, null, 'edit_reply') ||
833							$reply['reply_creator'] == $GLOBALS['egw_info']['user']['account_id'] && $this->check_rights($this->field_acl['edit_own_reply'], null, null, null, 'edit_own_reply'))
834					{
835						$reply['class'] = 'editable';
836					}
837				}
838			}
839		}
840		if ($content['num_replies'] && (!array_key_exists(0,$content['replies']) || $content['replies'][0]))
841    {
842        array_unshift($content['replies'],false);
843        array_unshift($preserve['replies'],false);
844    }	// need array index starting with 1!
845		$content['no_comment_visibility'] = !$this->check_rights(TRACKER_ADMIN|TRACKER_TECHNICIAN|TRACKER_ITEM_ASSIGNEE,null,null,null,'no_comment_visibility') ||
846			!$this->allow_restricted_comments;
847
848		// Toggle editable comments
849		$content['editable_comments'] = $this->check_rights($this->field_acl['edit_reply'], null, null, null, 'edit_reply') ||
850				 $this->check_rights($this->field_acl['edit_own_reply'], null, null, null, 'edit_own_reply')
851				? 'editable' : '';
852
853		// Context menu
854		$tpl->set_cell_attribute('replies', 'actions', array(
855			'replies_edit' => array(
856				'icon' => 'edit',
857				'caption' => 'Edit',
858				'allowOnMultiple' => false,
859				'onExecute' => 'javaScript:app.tracker.reply_edit',
860				'enableClass' => 'editable',
861				'hideOnDisabled' => true
862			),
863			'replies_files' => array(
864				'icon' => 'filemanager/navbar',
865				'caption' => 'Files',
866				'allowOnMultiple' => false,
867				'onExecute' => 'javaScript:app.tracker.reply_files',
868				'enableClass' => 'editable',
869				'hideOnDisabled' => true
870			),
871		));
872	}
873
874	/**
875	 * query rows for the nextmatch widget
876	 *
877	 * @param array $query with keys 'start', 'search', 'order', 'sort', 'col_filter'
878	 *	For other keys like 'filter', 'cat_id' you have to reimplement this method in a derived class.
879	 * @param array &$rows returned rows/competitions
880	 * @param array &$readonlys eg. to disable buttons based on Acl
881	 * @return int total number of rows
882	 */
883	function get_rows(&$query_in,&$rows,&$readonlys)
884	{
885		if (!$this->allow_voting && $query_in['order'] == 'votes' ||	// in case the tracker-config changed in that session
886			!$this->allow_bounties && $query_in['order'] == 'bounties') $query_in['order'] = 'tr_id';
887
888		$query = $query_in;
889		$old_query = Api\Cache::getSession('tracker',$query['session_for'] ? $query['session_for'] : 'index'.($query_in['only_tracker'] ? '-'.$query_in['only_tracker'] : ''));
890		if (!$query['csv_export'])	// do not store query for csv-export in session
891		{
892			Api\Cache::setSession('tracker',$query['session_for'] ? $query['session_for'] : 'index'.($query_in['only_tracker'] ? '-'.$query_in['only_tracker'] : ''),
893				array_diff_key ($query, array_flip(array('rows','actions','action_links','placeholder_actions'))));
894		}
895		// save the state of the index page (filters) in the user prefs
896		// need to save state, before resolving diverse col-filters, eg. to all group-members or sub-cats
897		$state = serialize(array(
898			'cat_id'     => $query['cat_id'],	// cat
899			'filter'     => $query['filter'],	// dates
900			'filter2'    => $query['filter2'],	// version
901			'order'      => $query['order'],
902			'sort'       => $query['sort'],
903			'num_rows'   => $query['num_rows'],
904			'col_filter' => array(
905				'tr_tracker'  => $query['col_filter']['tr_tracker'],
906				'tr_creator'  => $query['col_filter']['tr_creator'],
907				'tr_assigned' => $query['col_filter']['tr_assigned'],
908				'tr_status'   => $query['col_filter']['tr_status'],
909			),
910		));
911		if (!$query['csv_export'] && !$query['action'] && $GLOBALS['egw']->session->session_flags != 'A' &&	// store the current state of non-anonymous users in the prefs
912			$state != $GLOBALS['egw_info']['user']['preferences']['tracker']['index_state'])
913		{
914			//$msg .= "save the index state <br>";
915			$GLOBALS['egw']->preferences->add('tracker','index_state',$state);
916			// save prefs, but do NOT invalid the cache (unnecessary)
917			$GLOBALS['egw']->preferences->save_repository(false,'user',false);
918		}
919
920		$GLOBALS['egw']->session->commit_session();
921		$tracker = $query['col_filter']['tr_tracker'];
922
923		// Re-do actions on tracker or category change
924		if($old_query['col_filter']['tr_tracker'] != $tracker ||
925				$old_query['cat_id'] != $query['cat_id'])
926		{
927			$query_in['actions'] = $this->get_actions(
928				is_array($tracker) ? $tracker[0] : $tracker,
929				is_array($query['cat_id']) ? $query['cat_id'][0] : $query['cat_id']
930			);
931		}
932
933		// handle action and linked filter (show only entries linked to a certain other entry)
934		$link_filters = array();
935		$links = array();
936		if ($query['col_filter']['linked'])
937		{
938			$link_filters['linked'] = $query['col_filter']['linked'];
939			$links['linked'] = array();
940			unset($query['col_filter']['linked']);
941		}
942		if($query['action'] && in_array($query['action'], array_keys($GLOBALS['egw_info']['apps'])) && $query['action_id'])
943		{
944			$link_filters['action'] = array('app'=>$query['action'], 'id' => $query['action_id']);
945			$links['action'] = array();
946		}
947		foreach($link_filters as $key => $link)
948		{
949			if(!is_array($link))
950			{
951				// Legacy string style
952				list($app,$id) = explode(':',$link);
953			}
954			else
955			{
956				// Full info
957				$app = $link['app'];
958				$id = $link['id'];
959			}
960			if(!is_array($id)) $id = explode(',',$id);
961			if (!($linked = Link::get_links_multiple($app,$id,true,'tracker')))
962			{
963				$rows = array();	// no entries linked to selected link --> no rows to return
964				$this->get_rows_options($rows, $tracker);
965				return 0;
966			}
967
968
969			foreach($linked as $infos)
970			{
971				$links[$key] = array_merge($links[$key],$infos);
972			}
973			$links[$key] = array_unique($links[$key]);
974			if($key == 'linked')
975			{
976				$linked = array('app' => $app, 'id' => $id, 'title' => (count($id) == 1 ? Link::title($app, $id) : lang('multiple')));
977			}
978		}
979		if(count($links))
980		{
981			$query['col_filter']['tr_id'] = count($links) > 1 ? call_user_func_array('array_intersect', $links) : $links[$key];
982		}
983
984		// Explode multiples into array
985		if(!is_array($tracker) && strpos($tracker,',') !== false)
986		{
987			$tracker = $query['col_filter']['tr_tracker'] = explode(',',$query['col_filter']['tr_tracker']);
988		}
989		if (!($query['col_filter']['cat_id'] = $query['cat_id'])) unset($query['col_filter']['cat_id']);
990		if (!($query['col_filter']['tr_version'] = $query['filter2'])) unset($query['col_filter']['tr_version']);
991
992		if (!($query['col_filter']['tr_creator'])) unset($query['col_filter']['tr_creator']);
993
994		if ($query['col_filter']['tr_assigned'] < 0)	// resolve groups with it's members
995		{
996			$query['col_filter']['tr_assigned'] = $GLOBALS['egw']->accounts->members($query['col_filter']['tr_assigned'],true);
997			$query['col_filter']['tr_assigned'][] = $query_in['col_filter']['tr_assigned'];
998		}
999		elseif($query['col_filter']['tr_assigned'] === 'not')
1000		{
1001			$query['col_filter']['tr_assigned'] = null;
1002		}
1003		elseif(!$query['col_filter']['tr_assigned'])
1004		{
1005			unset($query['col_filter']['tr_assigned']);
1006		}
1007
1008		if (empty($query['col_filter']['tr_tracker']))
1009		{
1010			$tracker = $query['col_filter']['tr_tracker'] = array_keys($this->trackers);
1011		}
1012
1013		// Get list of currently displayed trackers, so we can get all valid statuses
1014		if ($tracker)
1015		{
1016			$trackers = is_array($tracker) ? $tracker : array($tracker);
1017		}
1018		else
1019		{
1020			$trackers = array();
1021		}
1022
1023		//echo "<p align=right>uitracker::get_rows() order='$query[order]', sort='$query[sort]', search='$query[search]', start=$query[start], num_rows=$query[num_rows], col_filter=".print_r($query['col_filter'],true)."</p>\n";
1024		$total = parent::get_rows($query,$rows,$readonlys,$this->allow_voting||$this->allow_bounties);	// true = count votes and/or bounties
1025		$prio_labels = $prio_tracker = $prio_cat = null;
1026		foreach($rows as $n => $row)
1027		{
1028			// Check if this is a new (unseen) ticket for the current user
1029			if (self::seen($row, false))
1030			{
1031				$rows[$n]['seen_class'] = 'tracker_seen';
1032			}
1033			else
1034			{
1035				$rows[$n]['seen_class'] = 'tracker_unseen';
1036			}
1037
1038			// Check rights for changing group via context menu, action looks for the CSS class
1039			if($this->check_rights($this->field_acl['tr_group'], null, $row))
1040			{
1041				$rows[$n]['class'] .= 'group_action';
1042			}
1043			switch ($this->enabled_color_code_for)
1044			{
1045				case 'tracker':
1046					$rows[$n]['enabled_color_code'] = $row['tr_tracker'];
1047					break;
1048				case 'cat':
1049					$rows[$n]['enabled_color_code'] = $row['cat_id'];
1050					break;
1051				case 'version':
1052					$rows[$n]['enabled_color_code'] = $row['tr_version'];
1053					break;
1054				default:
1055			}
1056			$trackers[] = $row['tr_tracker'];
1057
1058			// show the right tracker and/or cat specific priority label
1059			if ($row['tr_priority'])
1060			{
1061				if (is_null($prio_labels) || $this->priorities && ($row['tr_tracker'] != $prio_tracker || $row['cat_id'] != $prio_cat))
1062				{
1063					$prio_labels = $this->get_tracker_priorities($prio_tracker=$row['tr_tracker'],$prio_cat = $row['cat_id']);
1064					if ($prio_labels === self::$stock_priorities)	// show only the numbers for the stock priorities
1065					{
1066						$prio_labels = array_combine(array_keys(self::$stock_priorities),array_keys(self::$stock_priorities));
1067					}
1068				}
1069				$rows[$n]['prio_label'] = $prio_labels[$row['tr_priority']];
1070			}
1071			if (isset($rows[$n]['tr_description']))
1072			{
1073				if($rows[$n]['tr_edit_mode'] == 'ascii')
1074				{
1075					$rows[$n]['tr_description'] = htmlspecialchars($rows[$n]['tr_description']);
1076				}
1077				$rows[$n]['tr_description'] = nl2br(trim($rows[$n]['tr_description']));
1078			}
1079			if ($row['overdue'] && !$row['tr_closed']) $rows[$n]['overdue_class'] = 'tracker_overdue';
1080			if ($row['bounties']) $rows[$n]['currency'] = $this->currency;
1081
1082			if (isset($GLOBALS['egw_info']['user']['apps']['timesheet']))
1083			{
1084				unset($links);
1085				if (($links = Link::get_links('tracker',$row['tr_id'])) &&
1086					isset($GLOBALS['egw_info']['user']['apps']['timesheet']))
1087				{
1088					// loop through all links of the entries
1089					$timesheets = array();
1090					foreach ($links as $link)
1091					{
1092						if ($link['app'] == 'projectmanager')
1093						{
1094							//$info['pm_id'] = $link['id'];
1095						}
1096						if ($link['app'] == 'timesheet') $timesheets[] = $link['id'];
1097					}
1098					if (isset($GLOBALS['egw_info']['user']['apps']['timesheet']))
1099					{
1100						$sum = ExecMethod('timesheet.timesheet_bo.sum',$timesheets);
1101						$rows[$n]['tr_sum_timesheets'] = $sum['duration'];
1102					}
1103				}
1104			}
1105			// do NOT display public tickets with "No", just display "Yes" for private ticktes
1106			if ((string)$row['tr_private'] === '0') $rows[$n]['tr_private'] = '';
1107
1108			//_debug_array($rows[$n]);
1109			//echo "<p>".$this->trackers[$row['tr_tracker']]."</p>";
1110			$id=$row['tr_id'];
1111		}
1112
1113		$this->get_rows_options($rows,$tracker,$trackers);
1114
1115		// disable start date / due date column, if disabled in config
1116		if(!$this->show_dates)
1117		{
1118			$rows['no_tr_startdate_tr_duedate'] = true;
1119		}
1120
1121		return $total;
1122	}
1123
1124	/**
1125	 * Selectbox options vary depending on the selected tracker.
1126	 *
1127	 * @param Array $rows List of rows, we'll add the sel_options in
1128	 * @param String[] $tracker List of tracker IDs
1129	 */
1130	protected function get_rows_options(&$rows, $selected_trackers, $visible_trackers=array())
1131	{
1132		if(!is_array($selected_trackers) && strpos($selected_trackers,',') !== false)
1133		{
1134			$tracker = explode(',',$selected_trackers);
1135		}
1136		else
1137		{
1138			$tracker = (Array)$selected_trackers;
1139		}
1140		$rows['sel_options']['tr_assigned'] = array('not' => lang('Not assigned'));
1141
1142		// Add allowed staff
1143		foreach((array)$tracker as $tr_id)
1144		{
1145			$rows['sel_options']['tr_assigned'] += $this->get_staff($tr_id,2,$this->allow_assign_users?'usersANDtechnicians':'technicians');
1146		}
1147		$rows['sel_options']['assigned'] = $rows['sel_options']['tr_assigned']; // For context menu popup
1148		unset($rows['sel_options']['assigned']['not']);
1149
1150		$cats =array('' => lang('All categories'));
1151		$versions =  $resolutions = $statis = array();
1152		foreach((array)$tracker as $tr_id)
1153		{
1154			$versions += $this->get_tracker_labels('version',$tr_id);
1155			$cats += $this->get_tracker_labels('cat',$tr_id);
1156			$resolutions += $this->get_tracker_labels('resolution',$tr_id);
1157			$statis += $this->get_tracker_stati($tr_id);
1158		}
1159
1160		$trackers = array_unique($visible_trackers);
1161		if($trackers)
1162		{
1163			foreach($trackers as $tracker_id)
1164			{
1165				$statis += $this->get_tracker_stati($tracker_id);
1166				$resolutions += $this->get_tracker_labels('resolution',$tracker_id);
1167			}
1168		}
1169
1170		$rows['sel_options']['tr_status'] = $this->filters+$statis;
1171		$rows['sel_options']['cat_id'] = $cats;
1172		$rows['sel_options']['filter2'] = array(lang('All versions'))+$versions;
1173		$rows['sel_options']['tr_version'] =& $versions;
1174		$rows['sel_options']['tr_resolution'] =& $resolutions;
1175
1176		$rows['is_admin'] = $this->is_admin($tracker);
1177		if ($this->is_admin($tracker))
1178		{
1179			$rows['sel_options']['canned_response'] = $this->get_tracker_labels('response',$tracker);
1180			$rows['sel_options']['tr_status_admin'] =& $statis;
1181		}
1182		$rows['no_votes'] = !$this->allow_voting;
1183		if (!$this->allow_voting)
1184		{
1185			$query_in['options-selectcols']['votes'] = false;
1186		}
1187		$rows['no_bounties'] = !$this->allow_bounties;
1188		if (!$this->allow_bounties)
1189		{
1190			$query_in['options-selectcols']['bounties'] = false;
1191		}
1192
1193		$rows['no_cat_id'] = !!$rows['col_filter']['cat_id'];
1194
1195		// enable tracker column if all trackers are shown
1196		$rows['no_tr_tracker'] = ($tracker && count($tracker) == 1);
1197	}
1198
1199	/**
1200	 * Hook for timesheet to set some extra data and links
1201	 *
1202	 * @param array $data
1203	 * @param int $data[id] tracker_id
1204	 * @return array with key => value pairs to set in new timesheet and link_app/link_id arrays
1205	 */
1206	function timesheet_set($data)
1207	{
1208		$set = array();
1209		if ((int)$data['id'] && ($ticket = $this->read($data['id'])))
1210		{
1211			// Timesheet and files are always excluded
1212			$excluded_apps = array('timesheet',Link::VFS_APPNAME) + $this->exclude_app_on_timesheetcreation;
1213
1214			//error_log(__METHOD__.__LINE__.$this->exclude_app_on_timesheetcreation);
1215			foreach(Link::get_links('tracker',$ticket['tr_id'],'','link_lastmod DESC',true) as $link)
1216			{
1217				if (!in_array($link['app'], $excluded_apps))
1218				{
1219					$set['link_app'][] = $link['app'];
1220					$set['link_id'][]  = $link['id'];
1221				}
1222			}
1223		}
1224		return $set;
1225	}
1226
1227	/**
1228	 * Hook for InfoLog to set some extra data and links
1229	 *
1230	 * @param array $data
1231	 * @param int $data[id] tracker_id
1232	 * @return array with key => value pairs to set in new infolog and link_app/link_id arrays
1233	 */
1234	function infolog_set($data)
1235	{
1236		if (!($tracker = $this->read($data['id'])))
1237		{
1238			return array();
1239		}
1240		$set = array(
1241			'info_subject' => $tracker['tr_summary'],
1242			'info_des'     => $tracker['tr_description'],
1243			'info_contact' => 'tracker:'.$tracker['tr_id'],
1244		);
1245		// copy links
1246		foreach(Link::get_links('tracker',$tracker['tr_id'],'','link_lastmod DESC',true) as $link)
1247		{
1248			$set['link_app'][] = $link['app'];
1249			$set['link_id'][]  = $link['id'];
1250
1251			// prefer addressbook or projectmanager link as primary contact over default of this ticket
1252			if (in_array($link['app'], array('addressbook','projectmanager')) &&
1253				strpos($set['info_contact'], 'addressbook:') !== 0)
1254			{
1255				$set['info_contact'] = $link['app'].':'.$link['id'];
1256			}
1257		}
1258		// copy same named customfields
1259		foreach(Api\Storage\Customfields::get('infolog') as $name => $nul)
1260		{
1261			unset($nul);
1262			if(array_key_exists('#'.$name, $tracker))
1263			{
1264				$set['#'.$name] = $tracker['#'.$name];
1265			}
1266		}
1267		return $set;
1268	}
1269	/**
1270	 * Check if a ticket has already been seen
1271	 *
1272	 * @param array $data =null Ticket data
1273	 * @param boolean $update =false Set ticket as seen when true
1274	 * @param boolean $been_seen =true Mark the ticket as seen/unseen by current user
1275	 * @return boolean true=seen before false=new ticket
1276	 */
1277	function seen(&$data, $update=false, $been_seen = true)
1278	{
1279		$seen = array();
1280		if ($data['tr_seen']) $seen = unserialize($data['tr_seen']);
1281		if ($update === false)
1282		{
1283			return in_array($this->user, $seen);
1284		}
1285		if($been_seen)
1286		{
1287			$seen[] = $this->user;
1288		}
1289		else
1290		{
1291			$key = array_search($this->user,$seen);
1292			if($key !== false)
1293			{
1294				unset($seen[$key]);
1295			}
1296		}
1297		$this->db->update('egw_tracker', array('tr_seen' => serialize(array_unique($seen))),
1298			array('tr_id' => $data['tr_id']),__LINE__,__FILE__,'tracker');
1299		return false; // This time still false...
1300	}
1301
1302	/**
1303	 * Show a tracker
1304	 *
1305	 * @param array $content =null eTemplate content
1306	 * @param int $tracker =null id of tracker
1307	 * @param string $msg =''
1308	 * @param int $only_tracker =null show only the given tracker and not tracker-selection
1309	 * @param boolean $return_html =false if set to true, html content returned
1310	 * @return string html-content, if sitemgr otherwise null
1311	 */
1312	function index($content=null,$tracker=null,$msg='',$only_tracker=null, $return_html=false)
1313	{
1314		//_debug_array($this->trackers);
1315		if (!is_array($content))
1316		{
1317			if ($_GET['tr_id'])
1318			{
1319				if (!$this->read($_GET['tr_id']))
1320				{
1321					$msg = lang('Tracker item not found !!!');
1322				}
1323				else
1324				{
1325					return $this->edit(null,'',false);	// false = use no popup
1326				}
1327			}
1328			if (!$msg && $_GET['msg']) $msg = $_GET['msg'];
1329			if ($only_tracker && isset($this->trackers[$only_tracker]))
1330			{
1331				$tracker = $only_tracker;
1332			}
1333			else
1334			{
1335				$only_tracker = null;
1336			}
1337			// if there is no tracker specified, try the tracker submitted
1338			if (!$tracker && (int)$_GET['tracker']) $tracker = $_GET['tracker'];
1339			// if there is still no tracker, use the last tracker that was applied and saved to/with the view with the appsession
1340			if (!$tracker && ($state=  Api\Cache::getSession('tracker','index'.($only_tracker ? '-'.$only_tracker : ''))))
1341			{
1342			      $tracker= is_array($state['col_filter']['tr_tracker']) ?
1343						  $state['col_filter']['tr_tracker'][0] : $state['col_filter']['tr_tracker'];
1344			}
1345		}
1346		else
1347		{
1348			$only_tracker = $content['only_tracker']; unset($content['only_tracker']);
1349			$tracker = $content['nm']['col_filter']['tr_tracker'];
1350			$this->called_by = $content['called_by']; unset($content['called_by']);
1351
1352			if (is_array($content) && isset($content['nm']['rows']['document']))  // handle insert in default document button like an action
1353			{
1354				$id = @key($content['nm']['rows']['document']);
1355				$content['nm']['action'] = 'document';
1356				$content['nm']['selected'] = array($id);
1357			}
1358			if ($content['admin_popup'] && $content['nm']['action'] == 'admin')
1359			{
1360				$content['nm']['action'] = $content['admin_popup'];
1361			}
1362			// Clear multiple action popup
1363			unset($content['admin']);
1364
1365			if($content['nm']['action'])
1366			{
1367				if (!count($content['nm']['selected']) && !$content['nm']['select_all'])
1368				{
1369					$msg = lang('You need to select some entries first');
1370				}
1371				else
1372				{
1373					// Some processing to add values in for links and cats
1374					$multi_action = $content['nm']['action'];
1375					// Action has an additional action - add / delete, etc.  Buttons named <multi-action>_action[action_name]
1376					if(in_array($multi_action, array('link', 'assigned','group')))
1377					{
1378						$action = $content[$multi_action.'_popup'];
1379						$content['nm']['action'] .= '_' . key($action[$multi_action . '_action']);
1380
1381						// Action handling function wants a single string value, so mush it together
1382						if(is_array($action[$multi_action]))
1383						{
1384							if($multi_action == 'link')
1385							{
1386								$action[$multi_action] = $action[$multi_action]['app'] . ':' . $action[$multi_action]['id'];
1387							}
1388							else
1389							{
1390								$action[$multi_action] = implode(',',$action[$multi_action]);
1391							}
1392						}
1393						$content['nm']['action'] .= '_' . $action[$multi_action];
1394						unset($content[$multi_action]);
1395						unset($content[$multi_action.'_popup']);
1396					}
1397					$success = $failed = $action_msg = null;
1398					if ($this->action($content['nm']['action'],$content['nm']['selected'],$content['nm']['select_all'],
1399						$success,$failed,$action_msg,'index',$msg,$content['nm']['checkboxes']['no_notifications']))
1400					{
1401						$msg .= lang('%1 entries %2',$success,$action_msg);
1402					}
1403					else
1404					{
1405						if(is_null($msg) || $msg == '')
1406						{
1407							$msg = lang('%1 entries %2, %3 failed because of insufficent rights !!!',$success,$action_msg,$failed);
1408						}
1409					}
1410				}
1411			}
1412		}
1413
1414		if (!$tracker) $tracker = $content['nm']['col_filter']['tr_tracker'];
1415		$sel_options = array(
1416			'tr_tracker'  => $this->trackers,
1417			'tr_status'   => $this->filters + $this->get_tracker_stati($tracker),
1418			'tr_priority' => $this->get_tracker_priorities($tracker,$content['cat_id']),
1419			'tr_resolution' => $this->get_tracker_labels('resolution',$tracker),
1420			// Still need to provide options for the column filter
1421			'tr_private'  => array('No', 'Yes'),
1422		);
1423		if (($escalations = ExecMethod2('tracker.tracker_escalations.query_list','esc_title','esc_id')))
1424		{
1425			$sel_options['esc_id']['already escalated'] = $escalations;
1426			foreach($escalations as $esc_id => $label)
1427			{
1428				$sel_options['esc_id']['matching filter']['-'.$esc_id] = $label;
1429			}
1430		}
1431		// Merge print
1432		if ($GLOBALS['egw_info']['user']['preferences']['tracker']['document_dir'])
1433		{
1434			$documents = tracker_merge::get_documents($GLOBALS['egw_info']['user']['preferences']['tracker']['document_dir']);
1435			if($documents)
1436			{
1437				$sel_options['action'][lang('Insert in document').':'] = $documents;
1438			}
1439		}
1440
1441		if (!is_array($content)) $content = array();
1442		$content['nm'] = Api\Cache::getSession('tracker', $this->called_by ? $this->called_by : 'index'.($only_tracker ? '-'.$only_tracker : ''));
1443		$content['msg'] = $msg;
1444		$content['status_help'] = !$this->pending_close_days ? lang('Pending items never get close automatic.') :
1445				lang('Pending items will be closed automatic after %1 days without response.',$this->pending_close_days);
1446
1447		if (!is_array($content['nm']) || !$content['nm']['get_rows'])
1448		{
1449			$date_filters = array(lang('Date filter'));
1450			foreach(array_keys($this->date_filters) as $name)
1451			{
1452				$date_filters[$name] = lang($name);
1453			}
1454			$date_filters['custom'] = lang('custom');
1455			$content['nm'] = array(
1456				'get_rows'       =>	'tracker.tracker_ui.get_rows',
1457				'cat_is_select'  => 'no_lang',
1458				'filter'         => 0,  // all
1459				'options-filter' => $date_filters,
1460				'filter_onchange' => "app.tracker.filter_change();",
1461				//'filter_label'   => lang('Date filter'),
1462				'filter_no_lang'=> true,
1463				'filter2'        => 0,	// all
1464				'filter2_tags'	=> true,
1465				//'filter2_label'  => lang('Version'),
1466				'filter2_no_lang'=> true,
1467				'order'          =>	$this->allow_bounties ? 'bounties' : ($this->allow_voting ? 'votes' : 'tr_id'),// IO name of the column to sort after (optional for the sortheaders)
1468				'sort'           =>	'DESC',// IO direction of the sort: 'ASC' or 'DESC'
1469				'options-tr_assigned' => array('not' => lang('Noone')),
1470				'col_filter'     => array(
1471					'tr_status'  => 'not-closed',	// default filter: not closed
1472				),
1473	 			'only_tracker'   => $only_tracker,
1474	 			'default_cols'   => '!esc_id,legacy_actions,tr_summary_tr_description,tr_resolution,tr_completion,tr_sum_timesheets,votes,bounties',
1475				'row_id'         => 'tr_id',
1476				'row_modified'   => 'tr_modified',
1477				'placeholder_actions' => array('add')
1478			);
1479			switch($this->enabled_color_code_for)
1480			{
1481				case 'cat':
1482					$content['nm']['cat_id_class'] = 'cat_';
1483					break;
1484				case 'version':
1485					$content['nm']['filter2_class'] = 'cat_';
1486					break;
1487				default:
1488			}
1489			// use the state of the last session stored in the user prefs
1490			if (!$this->called_by && ($state = @unserialize($GLOBALS['egw_info']['user']['preferences']['tracker']['index_state'])))
1491			{
1492				unset($state['header_left']); unset($state['header_right']);
1493				$content['nm'] = array_merge($content['nm'],$state);
1494				$tracker = $content['nm']['col_filter']['tr_tracker'];
1495			}
1496			elseif (!$this->called_by && !$tracker)
1497			{
1498				reset($this->trackers);
1499				$tracker = @key($this->trackers);
1500			}
1501			// disable times column, if no timesheet rights
1502			if (!isset($GLOBALS['egw_info']['user']['apps']['timesheet']))
1503			{
1504				$content['nm']['options-selectcols']['tr_sum_timesheets'] = false;
1505			}
1506			// disable start date / due date column, if disabled in config
1507			if(!$this->show_dates)
1508			{
1509				// Need to set each field so parser takes the whole column
1510				$content['nm']['options-selectcols']['tr_startdate'] = false;
1511				$content['nm']['options-selectcols']['tr_duedate'] = false;
1512			}
1513			$content['nm']['no_votes'] = !$this->allow_voting;
1514			$content['nm']['no_bounties'] = !$this->allow_bounties;
1515			$content['nm']['no_tr_sum_timesheets'] = false;
1516		}
1517		if (!$content['nm']['session_for'] && $this->called_by) $content['nm']['session_for'] = $this->called_by;
1518		if($_GET['search'])
1519		{
1520			$content['nm']['search'] = $_GET['search'];
1521		}
1522		// if there is only one tracker, use that one and do NOT show the selectbox
1523		if (count($this->trackers) == 1)
1524		{
1525			reset($this->trackers);
1526			$tracker = @key($this->trackers);
1527			$readonlys['nm']['col_filter[tr_tracker]'] = true;
1528		}
1529		if (!$tracker)
1530		{
1531			$tracker = $content['nm']['col_filter']['tr_tracker'] = '';
1532		}
1533		else
1534		{
1535			$content['nm']['col_filter']['tr_tracker'] = $tracker;
1536		}
1537
1538		//
1539		// disable favories dropdown button, if not running as infolog
1540		if ($this->called_as || $content['nm']['session_for'])
1541		{
1542			$content['nm']['favorites'] = false;
1543		}
1544		else
1545		{
1546			$content['nm']['favorites'] = true; // Enable favorites
1547		}
1548		$content['duration_format'] = ','.$this->duration_format;
1549
1550		$content['is_admin'] = $this->is_admin($tracker);
1551		//_debug_array($content);
1552		$readonlys['add'] = $readonlys['nm']['add'] = !$this->check_rights($this->field_acl['add'],$tracker,null,null,'add');
1553		$tpl = new Etemplate();
1554		if (!$tpl->sitemgr || !$tpl->read('tracker.index.sitemgr'))
1555		{
1556			$tpl->read('tracker.index');
1557		}
1558
1559		// Apply link / avoid DOM conflicts
1560		if($this->called_by)
1561		{
1562			$content['nm'] = array_merge($content['nm'], Api\Cache::getSession('tracker', $this->called_by));
1563		}
1564
1565		$content['nm']['actions'] = $this->get_actions($tracker, $content['cat_id']);
1566
1567		// disable filemanager icon, if user has no access to it
1568		$readonlys['filemanager/navbar'] = !isset($GLOBALS['egw_info']['user']['apps']['filemanager']);
1569
1570		// Disable actions if there are none
1571		if(count($sel_options['action']) == 0)
1572		{
1573			$tpl->disable_cells('action', true);
1574			$tpl->disable_cells('use_all', true);
1575		}
1576
1577		// Show only own groups in group popup if queue Acl
1578		if($this->enabled_queue_acl_access)
1579		{
1580			$group = explode(',',$tpl->get_cell_attribute('group', 'size'));
1581			$group[1] = 'owngroups';
1582			$tpl->set_cell_attribute('group', 'size', implode(',',$group));
1583		}
1584		Framework::includeJS('.','app','tracker');
1585		// add scrollbar to long description, if user choose so in his prefs
1586		/* @kl: why is an if used, if it is effectily commented by a semicolon?
1587		if ($this->prefs['limit_des_lines'] > 0 || (string)$this->prefs['limit_des_lines'] == '');
1588		*/
1589		{
1590			$content['css'] .= '<style type="text/css">@media screen { .trackerDes {  '.
1591				($this->prefs['limit_des_width']?'max-width:'.$this->prefs['limit_des_width'].'em;':'').' max-height: '.
1592				(($this->prefs['limit_des_lines'] ? $this->prefs['limit_des_lines'] : 5) * 1.35).	// dono why em is not real lines
1593				'em; overflow: auto; }}
1594@media screen { .colfullWidth {
1595width:100%;
1596}</style>';
1597		}
1598
1599		$preserve = array(
1600			'only_tracker' => $only_tracker,
1601			'called_by' => $this->called_by
1602		);
1603		if ($this->enabled_color_code_for == 'tracker') $tpl->setElementAttribute ('nm[col_filter][tr_tracker]', 'value_class', 'cat_');
1604		return $tpl->exec('tracker.tracker_ui.index',$content,$sel_options,$readonlys,$preserve,$return_html);
1605	}
1606
1607	/**
1608	 * Get actions / context menu items
1609	 *
1610	 * @param int $tracker =null
1611	 * @param int $cat_id =null
1612	 * @return array see nextmatch_widget::get_actions()
1613	 */
1614	public function get_actions($tracker=null, $cat_id=null)
1615	{
1616		for($i = 0; $i <= 100; $i += 10)
1617		{
1618			$percent[$i] = $i.'%';
1619		}
1620		// Find the ID for 'Fixed' resolution, used below
1621		$resolution_fixed = key(array_filter($this->get_tracker_labels('resolution'), function($a) {
1622			return $a == 'Fixed';
1623		}));
1624		$actions = array(
1625			'open' => array(
1626				'caption' => 'Open',
1627				'default' => true,
1628				'allowOnMultiple' => false,
1629				'url' => 'menuaction=tracker.tracker_ui.edit&tr_id=$id',
1630				'popup' => Link::get_registry('tracker', 'add_popup'),
1631				'group' => $group=1,
1632				'onExecute' => Api\Header\UserAgent::mobile()?'javaScript:app.tracker.viewEntry':'',
1633				'mobileViewTemplate' => 'view?'.filemtime(Api\Etemplate\Widget\Template::rel2path('/tracker/templates/mobile/view.xet'))
1634			),
1635			'print' => array(
1636				'caption' => 'Print',
1637				'allowOnMultiple' => false,
1638				'onExecute' => 'javaScript:app.tracker.tprint',
1639				'group' => $group,
1640				'hideOnMobile' => true
1641			),
1642			'add' => array(
1643				'caption' => 'Add',
1644				'group' => $group,
1645				'children' => array(
1646					'new' => array(
1647						'caption' => 'New',
1648						'url' => 'menuaction=tracker.tracker_ui.edit',
1649						'popup' => Link::get_registry('tracker', 'add_popup'),
1650						'icon' => 'new',
1651					),
1652					'copy' => array(
1653						'caption' => 'Copy',
1654						'url' => 'menuaction=tracker.tracker_ui.edit&makecp=1&tr_id=$id',
1655						'popup' => Link::get_registry('tracker', 'add_popup'),
1656						'allowOnMultiple' => false,
1657						'icon' => 'copy',
1658					),
1659				),
1660				'hideOnMobile' => true
1661			),
1662			'no_notifications' => array(
1663				'caption' => 'Do not notify',
1664				'checkbox' => true,
1665				'hint' => 'Do not notify of these changes',
1666				'confirm_mass_selection' => "You are going to change %1 entries: Are you sure you want to send notifications about this change?",
1667				'group' => $group,
1668			),
1669			// modifying content of one or multiple infolog(s)
1670			'change' => array(
1671				'caption' => 'Change',
1672				'group' => ++$group,
1673				'icon' => 'edit',
1674				'disableClass' => 'rowNoEdit',
1675				'confirm_mass_selection' => true,
1676				'children' => array(
1677					'seen' => array(
1678						'caption' => 'Mark as read',
1679						'group' => 1,
1680					),
1681					'unseen' => array(
1682						'caption' => 'Mark as unread',
1683						'group' => 1,
1684					),
1685					'tracker' => array(
1686						'caption' => 'Tracker Queue',
1687						'prefix' => 'tracker_',
1688						'children' => $this->trackers,
1689						'enabled' => count($this->trackers) >= 1,
1690						'hideOnDisabled' => true,
1691						'icon' => 'tracker/navbar',
1692					),
1693					'cat' => array(
1694						'caption' => 'Category',
1695						'prefix' => 'cat_',
1696						'children' => $items=$this->get_tracker_labels('cat',$tracker),
1697						'enabled' => count($items) >= 1,
1698						'hideOnDisabled' => true,
1699					),
1700					'version' => array(
1701						'caption' => 'Version',
1702						'prefix' => 'version_',
1703						'children' => $items=$this->get_tracker_labels('version',$tracker),
1704						'enabled' => count($items) >= 1,
1705						'hideOnDisabled' => true,
1706					),
1707					'assigned' => array(
1708						'caption' => 'Assigned to',
1709						'icon' => 'users',
1710						'nm_action' => 'open_popup',
1711						'onExecute' => 'javaScript:app.tracker.change_assigned'
1712					),
1713					'priority' => array(
1714						'caption' => 'Priority',
1715						'prefix' => 'priority_',
1716						'children' => $items=$this->get_tracker_priorities($tracker,$cat_id),
1717						'enabled' => count($items) >= 1,
1718						'hideOnDisabled' => true,
1719					),
1720					'status' => array(
1721						'caption' => 'Status',
1722						'prefix' => 'status_',
1723						'children' => $items=$this->get_tracker_stati($tracker),
1724						'enabled' => count($items) >= 1,
1725						'hideOnDisabled' => true,
1726						'icon' => 'check',
1727					),
1728					'resolution' => array(
1729						'caption' => 'Resolution',
1730						'prefix' => 'resolution_',
1731						'children' => $items=$this->get_tracker_labels('resolution',$tracker), // ToDo: get tracker specific solutions as well, have them available only when applicable
1732						'enabled' => count($items) >= 1,
1733						'hideOnDisabled' => true,
1734					),
1735					'completion' => array(
1736						'caption' => 'Completed',
1737						'prefix' => 'completion_',
1738						'children' => $percent,
1739						'icon' => 'completed',
1740					),
1741					'group' => array(
1742						'caption' => 'Group',
1743						'nm_action' => 'open_popup',
1744						'enableClass' => 'group_action',
1745					),
1746					'link' => array(
1747						'caption' => 'Links',
1748						'nm_action' => 'open_popup',
1749					),
1750				),
1751				'hideOnMobile' => true
1752			),
1753			'close' => array(
1754				'caption' => 'Close',
1755				'icon' => 'check',
1756				'group' => $group,
1757				'disableClass' => 'rowNoClose',
1758				'confirm_mass_selection' => true,
1759			),
1760			'close_100_'.$resolution_fixed => array(
1761				'caption' => lang('Close') . ' - 100% ' . lang('fixed'),
1762				'icon' => 'check',
1763				'group' => $group,
1764				'disableClass' => 'rowNoClose',
1765				'confirm_mass_selection' => true,
1766			),
1767
1768			'admin' => array(
1769				'caption' => 'Multiple changes',
1770				'group' => $group,
1771				'enabled' => $this->is_admin($tracker),
1772				'hideOnDisabled' => true,
1773				'nm_action' => 'open_popup',
1774				'icon' => 'user',
1775			),
1776		);
1777		++$group;	// integration with other apps
1778		if ($GLOBALS['egw_info']['user']['apps']['filemanager'])
1779		{
1780			$actions['filemanager'] = array(
1781				'icon' => 'filemanager/navbar',
1782				'caption' => 'Filemanager',
1783				'url' => 'menuaction=filemanager.filemanager_ui.index&path=/apps/tracker/$id&ajax=true',
1784				'allowOnMultiple' => false,
1785				'group' => $group,
1786			);
1787		}
1788		if ($GLOBALS['egw_info']['user']['apps']['timesheet'])
1789		{
1790			$actions['timesheet'] = array(	// interactive add for a single event
1791				'icon' => 'timesheet/navbar',
1792				'caption' => 'Timesheet',
1793				'url' => 'menuaction=timesheet.timesheet_ui.edit&link_app[]=tracker&link_id[]=$id',
1794				'group' => $group,
1795				'allowOnMultiple' => false,
1796				'popup' => Link::get_registry('timesheet', 'add_popup'),
1797			);
1798		}
1799		if ($GLOBALS['egw_info']['user']['apps']['infolog'] && $this->allow_infolog)
1800		{
1801			$actions['infolog'] = array(
1802				'icon' => 'infolog/navbar',
1803				'caption' => 'InfoLog',
1804				'url' => 'menuaction=infolog.infolog_ui.edit&action=tracker&action_id=$id',
1805				'group' => $group,
1806				'allowOnMultiple' => false,
1807				'popup' => Link::get_registry('infolog', 'add_popup'),
1808			);
1809		}
1810
1811		$actions += EGroupware\Api\Link\Sharing::get_actions('tracker', $group);
1812		// ACL blocks most access right now TODO: allow access
1813		unset($actions['share']['children']['shareWritable']);
1814		unset($actions['share']['children']['shareFiles']);
1815		// Give a readonly & writable filemanager directory actions
1816		$actions['share']['children']['shareFilemanager']['caption'] = 'Readonly filemanager directory';
1817		$actions['share']['children']['shareWritableFilemanager'] = array_merge(
1818			$actions['share']['children']['shareFilemanager'],
1819			array('caption' => 'Writable filemanager directory',
1820					'hint' => 'Share the filemanager directory, allowing editing')
1821		);
1822
1823
1824		$actions['documents'] = tracker_merge::document_action(
1825			$this->prefs['document_dir'], ++$group, 'Insert in document', 'document_',
1826			$this->prefs['default_document']
1827		);
1828
1829		//echo "<p>".__METHOD__."($do_email, $tid_filter, $org_view)</p>\n"; _debug_array($actions);
1830		return $actions;
1831	}
1832
1833	/**
1834	 * imports a mail as Tracker
1835	 *
1836	 * @param array $mailContent = null mail content
1837	 * @return  array
1838	 */
1839	function mail_import(array $mailContent=null)
1840	{
1841		// It would get called from compose as a popup with egw_data
1842		if (!is_array($mailContent) && ($_GET['egw_data']))
1843		{
1844			// get the mail raw data
1845			Link::get_data ($_GET['egw_data']);
1846			return false;
1847		}
1848		if($this->htmledit && $mailContent['html_message'])
1849		{
1850			$message = $mailContent['html_message'];
1851		}
1852		else
1853		{
1854			// Wrap a pre tag if we are using html editor
1855			$message = $this->htmledit? "<pre>".$mailContent['message']."</pre>": $mailContent['message'];
1856		}
1857
1858		$this->edit($this->prepare_import_mail($mailContent['addresses'],
1859				$mailContent['subject'],
1860				$message,
1861				$mailContent['attachments'],
1862				$mailContent['entry_id']));
1863	}
1864
1865	/**
1866	 * apply an action to multiple tracker entries
1867	 *
1868	 * @param string|int $action 'status_to',set status of entries
1869	 * @param array $checked tracker id's to use if !$use_all
1870	 * @param boolean $use_all if true use all entries of the current selection (in the session)
1871	 * @param int &$success number of succeded actions
1872	 * @param int &$failed number of failed actions (not enought permissions)
1873	 * @param string &$action_msg translated verb for the actions, to be used in a message like %1 entries 'deleted'
1874	 * @param string|array $session_name 'index' or 'email', or array with session-data depending if we are in the main list or the popup
1875	 * @param string &$msg
1876	 * @param boolean $no_notification
1877	 * @return boolean true if all actions succeded, false otherwise
1878	 */
1879	function action($action,$checked,$use_all,&$success,&$failed,&$action_msg,$session_name,&$msg,$no_notification)
1880	{
1881		//echo '<p>'.__METHOD__."('$action',".array2string($checked).','.(int)$use_all.",...)</p>\n";
1882		$success = $failed = 0;
1883		if ($use_all)
1884		{
1885			// get the whole selection
1886			$query = is_array($session_name) ? $session_name : Api\Cache::getSession('tracker', $session_name);
1887
1888			if ($use_all)
1889			{
1890				@set_time_limit(0);			// switch off the execution time limit, as it's for big selections to small
1891				$query['num_rows'] = -1;	// all
1892				$readonlys = null;
1893				$this->get_rows($query,$checked,$readonlys);
1894				// $this->get_rows gives some extra data.
1895				foreach($checked as $row => $data)
1896				{
1897					unset($data);
1898					if(!is_numeric($row))
1899					{
1900						unset($checked[$row]);
1901					}
1902				}
1903			}
1904		}
1905
1906		if (is_array($action) && $action['update'])
1907		{
1908			unset($action['update']);
1909			// remove all 'No change'
1910			foreach($action as $name => $value)
1911			{
1912				if ($value === '') unset($action[$name]);
1913			}
1914			if (!count($checked) || !count($action))
1915			{
1916				$msg = lang('You need to select something to change AND some tracker items!');
1917				$failed = true;
1918			}
1919			else
1920			{
1921				foreach($checked as $tr_id)
1922				{
1923					if (!$this->read($tr_id)) continue;
1924					foreach($action as $name => $value)
1925					{
1926						if ($name == 'tr_status_admin') $name = 'tr_status';
1927						$this->data[$name] = $name == 'tr_assigned' && $value === 'not' ? NULL : $value;
1928					}
1929					if($no_notification) $this->data['no_notifications'] = true;
1930					if (!$this->save())
1931					{
1932						$success++;
1933					}
1934					else
1935					{
1936						$failed++;
1937					}
1938				}
1939				$action_msg = lang('updated');
1940			}
1941		}
1942		else
1943		{
1944			// Dialogs to get options
1945			list($action, $settings) = explode('_', $action, 2);
1946
1947			switch($action)
1948			{
1949				case 'close':
1950					$action_msg = lang('closed');
1951					if(is_string($settings)) // ex: closed-100-fixed
1952					{
1953						$settings = explode('_', $settings);
1954					}
1955					foreach($checked as $tr_id)
1956					{
1957						if (!$this->read($tr_id)) continue;
1958						$this->data['tr_status'] = tracker_bo::STATUS_CLOSED;
1959						if($no_notification) $this->data['no_notifications'] = true;
1960
1961						if($settings[0])
1962						{
1963							$this->data['tr_completion'] = $settings[0];
1964						}
1965						if($settings[1])
1966						{
1967							$this->data['tr_resolution'] = $settings[1];
1968						}
1969						if (!$this->save())
1970						{
1971							$success++;
1972						}
1973						else
1974						{
1975							$failed++;
1976						}
1977					}
1978					break;
1979				case 'seen':
1980				case 'unseen':
1981					$action_msg = lang($action);
1982					foreach($checked as $tr_id)
1983					{
1984						if (!$this->read($tr_id)) continue;
1985						self::seen($this->data, true, $action == 'seen');
1986						$success++;
1987					}
1988					break;
1989				case 'group':
1990					// Popup adds an extra param (add/delete) that group doesn't need
1991					list(,$settings) = explode('_',$settings);
1992				case 'tracker':
1993				case 'cat':
1994				case 'version':
1995				case 'priority':
1996				case 'status':
1997				case 'resolution':
1998				case 'completion':
1999					$action_msg = lang('updated');
2000					foreach($checked as $tr_id)
2001					{
2002						if (!$this->read($tr_id)) continue;
2003						$this->data[($action == 'cat' ? 'cat_id' : 'tr_'.$action)] = $settings;
2004						if($no_notification) $this->data['no_notifications'] = true;
2005						if (!$this->save())
2006						{
2007							$success++;
2008						}
2009						else
2010						{
2011							$failed++;
2012						}
2013					}
2014					break;
2015				case 'assigned':
2016					$action_msg = lang('updated');
2017					foreach($checked as $tr_id)
2018					{
2019						if (!$this->read($tr_id)) continue;
2020						list($add_remove, $idstr) = explode('_', $settings, 2);
2021						$ids = explode(',',$idstr);
2022						if($add_remove == 'ok')
2023						{
2024							$this->data['tr_assigned'] = $ids;
2025						}
2026						else
2027						{
2028							$this->data['tr_assigned'] = $add_remove == 'add' ?
2029								array_merge($this->data['tr_assigned'],$ids) :
2030								array_diff($this->data['tr_assigned'],$ids);
2031						}
2032						// No 0 allowed
2033						$this->data['tr_assigned'] = array_unique(array_diff($this->data['tr_assigned'], array(0)));
2034						if($no_notification) $this->data['no_notifications'] = true;
2035						if (!$this->save())
2036						{
2037							$success++;
2038						}
2039						else
2040						{
2041							$failed++;
2042						}
2043					}
2044					break;
2045
2046				case 'link':
2047					list($add_remove, $link) = explode('_', $settings, 2);
2048					list($app, $link_id) = explode(':', $link);
2049					if(!$link_id)
2050					{
2051						$msg = lang('You need to select an entry for linking.');
2052						break;
2053					}
2054					error_log("APp: $app ID: $link_id");
2055					$title = Link::title($app, $link_id);
2056					foreach($checked as $id)
2057					{
2058						if (!$this->read($id))
2059						{
2060							$failed++;
2061							continue;
2062						}
2063						if($add_remove == 'add')
2064						{
2065							$action_msg = lang('linked to %1', $title);
2066							if(Link::link('tracker', $id, $app, $link_id))
2067							{
2068								$success++;
2069							}
2070							else
2071							{
2072								$failed++;
2073							}
2074						}
2075						else
2076						{
2077							$action_msg = lang('unlinked from %1', $title);
2078							$count = Link::unlink(0, 'tracker', $id, '', $app, $link_id);
2079							$success += $count;
2080						}
2081					}
2082					return $failed == 0;
2083
2084				case 'document':
2085					if (!$settings) $settings = $GLOBALS['egw_info']['user']['preferences']['tracker']['default_document'];
2086					$document_merge = new tracker_merge();
2087					$msg = $document_merge->download($settings, $checked, '', $GLOBALS['egw_info']['user']['preferences']['tracker']['document_dir']);
2088					$failed = count($checked);
2089					return false;
2090			}
2091		}
2092		return !$failed;
2093	}
2094
2095	/**
2096	 * Fill in canned comment
2097	 *
2098	 * @param id Canned comment ID
2099	 */
2100	public function ajax_canned_comment($id, $htmlarea=true)
2101	{
2102		$response = Api\Json\Response::get();
2103
2104		if($htmlarea)
2105		{
2106			$response->call('app.tracker.canned_comment_response',nl2br($this->get_canned_response($id)));
2107		}
2108		else
2109		{
2110			$response->call('app.tracker.canned_comment_response', $this->get_canned_response($id));
2111		}
2112	}
2113
2114	/**
2115	 * Edit a comment
2116	 *
2117	 * @param value
2118	 * @param tr_id
2119	 * @param comment_id
2120	 */
2121	public function ajax_update_reply($value, $tr_id, $comment_id)
2122	{
2123		if(!$this->check_rights($this->field_acl['edit_reply'], null, (int)$tr_id) && !$this->check_rights($this->field_acl['edit_own_reply'], null, (int)$tr_id))
2124		{
2125			// No rights for any edit
2126			return false;
2127		}
2128
2129		if(!$this->check_rights($this->field_acl['edit_reply'], null, (int)$tr_id))
2130		{
2131			// Need to read ticket so we can get comment owner & verify
2132			$verified = false;
2133			$this->read((int)$tr_id);
2134			foreach($this->data['replies'] as $key => &$reply)
2135			{
2136				if($reply['reply_id'] == $comment_id &&
2137						$reply['reply_creator'] == $GLOBALS['egw_info']['user']['account_id'] &&
2138						$this->check_rights($this->field_acl['edit_own_reply'], null, null, null, 'edit_own_reply'))
2139				{
2140					$verified = true;
2141					break;
2142				}
2143			}
2144			if(!$verified)
2145			{
2146				return false;
2147			}
2148		}
2149
2150		// Update the comment
2151		$this->save_comment(array(
2152			'reply_id' => (int)$comment_id,
2153			'reply_message' => $value
2154		));
2155	}
2156
2157	/**
2158	 * shows tracker in other applications
2159	 *
2160	 * @param $args['location'] location of hooks: {addressbook|projects|calendar}_view
2161	 * @param $args['view']     menuaction to view, if location == 'infolog'
2162	 * @param $args['app']      app-name, if location == 'infolog'
2163	 * @param $args['view_id']  name of the id-var for location == 'infolog'
2164	 * @param $args[$args['view_id']] id of the entry
2165	 * this function can be called for any app, which should include infolog: \
2166	 * 	Api\Hooks::process(array( \
2167	 * 		 * 'location' => 'infolog', \
2168	 * 		 * 'app'      => <your app>, \
2169	 * 		 * 'view_id'  => <id name>, \
2170	 * 		 * <id name>  => <id value>, \
2171	 * 		 * 'view'     => <menuaction to view an entry in your app> \
2172	 * 	));
2173	 */
2174	public function hook_view($args)
2175	{
2176		// Load JS for tracker actions
2177		Framework::includeJS('.','app','tracker');
2178
2179		switch ($args['location'])
2180		{
2181			case 'addressbook_view':
2182				$app     = 'addressbook';
2183				$view_id = 'ab_id';
2184				// Just set the filter
2185				$state['action'] = $app;
2186				$state['action_id'] = $args[$view_id];
2187				Api\Cache::setSession('tracker', $app, $state);
2188				break;
2189		}
2190		if (!isset($app) || !isset($args[$view_id]))
2191		{
2192			return False;
2193		}
2194		$this->called_by = $app;	// for read/save_sessiondata, to have different sessions for the hooks
2195
2196		// Set to calling app, so actions wind up in the correct place client side
2197		$GLOBALS['egw_info']['flags']['currentapp'] = $app;
2198
2199		Api\Translation::add_app('tracker');
2200
2201		$this->index(null);
2202	}
2203
2204	/**
2205	 * Copy a given ticket (not storing it!)
2206	 *
2207	 * Taken care only configured fields get copied and certain fields never to copy (uid etc.).
2208	 *
2209	 * @param array& $content
2210	 */
2211	function copy(array &$content)
2212	{
2213		$id = $content['tr_id'];
2214
2215		// If original is closed, copy should be open
2216		if($content['tr_closed'] && $content['tr_completion'] == '100')
2217		{
2218			$content['tr_status'] = self::STATUS_OPEN;
2219			$content['tr_completion'] = 0;
2220			// Get default resolution
2221			$this->get_tracker_labels('resolution', $content['tr_tracker'], $content['tr_resolution']);
2222		}
2223
2224		$exclude_fields = array('tr_id', 'tr_closed', 'tr_seen',
2225			'tr_created', 'tr_modified', 'tr_modifier'
2226		);
2227		foreach ($exclude_fields as $field)
2228		{
2229			unset($content[$field]);
2230		}
2231		// startdate in the past --> set startdate
2232		if ($content['tr_startdate'] && $content['tr_startdate'] < Api\DateTime::to('now'))
2233		{
2234			$content['tr_startdate'] = Api\DateTime::to('now');
2235		}
2236		// duedate in the past --> unset it
2237		if (isset($content['tr_duedate']) && $content['tr_duedate'] < Api\DateTime::to('now'))
2238		{
2239			unset($content['tr_duedate']);
2240		}
2241
2242		if(!is_array($content['link_to'])) $content['link_to'] = array();
2243		$content['link_to']['to_app'] = 'tracker';
2244		$content['link_to']['to_id'] = 0;
2245		// Get links to be copied, if not excluded
2246		if (!in_array('link_to',$exclude_fields) || !in_array('attachments',$exclude_fields))
2247		{
2248			foreach(Link::get_links($content['link_to']['to_app'], $id) as $link)
2249			{
2250				if ($link['app'] != Link::VFS_APPNAME && !in_array('link_to', $exclude_fields))
2251				{
2252					Link::link('tracker', $content['link_to']['to_id'], $link['app'], $link['id'], $link['remark']);
2253				}
2254				elseif ($link['app'] == Link::VFS_APPNAME && !in_array('attachments', $exclude_fields))
2255				{
2256					Link::link('tracker', $content['link_to']['to_id'], Link::VFS_APPNAME, array(
2257						'tmp_name' => Link::vfs_path($link['app2'], $link['id2']).'/'.$link['id'],
2258						'name' => $link['id'],
2259					), $link['remark']);
2260				}
2261			}
2262		}
2263		$content['links'] = $content['link_to'];
2264
2265		$content['tr_owner'] = !(int)$content['owner'] || !$this->bo->check_perms(Acl::ADD,0,$content['owner']) ? $this->user : $this->owner;
2266
2267		// If current user has no permissions for creator, use them as creator
2268		$readonlys = $this->readonlys_from_acl();
2269		$content['tr_creator'] = $readonlys['tr_creator'] ? $this->user : $content['tr_creator'];
2270
2271		if (!empty($content['tr_summary']))
2272		{
2273			$content['tr_summary'] = lang('Copy of:').' '.$content['tr_summary'];
2274		}
2275
2276		$content['msg'] .= ($content['msg']?"\n":'').lang('%1 copied - the copy can now be edited', lang(Link::get_registry('tracker','entry')));
2277	}
2278
2279	/**
2280	 * Modify history to hide changes on restricted comments if the current user
2281	 * is not allowed to see them.
2282	 *
2283	 * @param array $data values for keys "data" (data) and "args":
2284	 *  values for keys "value", "rows" (reference) and "total" (reference)
2285	 */
2286	public function modify_history(array &$data)
2287	{
2288		// Is current user restricted?
2289		$this->read($data['value']['record_id']);
2290		$user = $GLOBALS['egw_info']['user']['account_id'];
2291		$is_admin = $this->is_admin($this->data['tr_tracker'], $user);
2292		$is_technician = $this->is_technician($this->data['tr_tracker'], $user);
2293
2294		$read_restricted = $is_admin || $is_technician || in_array($user, $this->data['tr_assigned']) ||
2295				// if assigned to a group, we need to check memberships of $user
2296				$GLOBALS['egw']->accounts->get_type($this->data['tr_assigned']) == 'g' &&
2297					in_array($this->data['tr_assigned'], $GLOBALS['egw']->accounts->memberships($user, true));
2298
2299		// Can read the hidden comments, no changes needed
2300		if($read_restricted)
2301		{
2302			return;
2303		}
2304
2305		// Hide restricted comments
2306		$remove_indexes = Array();
2307		foreach($data['rows'] as $index => $row)
2308		{
2309			if($row['status'] !== 'comment')
2310			{
2311				continue;
2312			}
2313			list(,$comment_id) = explode(': ',$row['new_value'][0]);
2314			$comment_index = array_search($comment_id, array_column($this->data['replies'],'reply_id'));
2315			$comment = $this->data['replies'][$comment_index];
2316
2317			if(!$comment || $comment_index === FALSE || $comment && $comment['reply_visible'])
2318			{
2319				$remove_indexes[] = $index;
2320			}
2321		}
2322		$data['rows'] = array_diff_key($data['rows'], array_flip($remove_indexes));
2323		$data['total'] -= count($remove_indexes);
2324	}
2325}
2326