Nextmatch   F
last analyzed

Complexity

Total Complexity 229

Size/Duplication

Total Lines 1173
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 443
dl 0
loc 1173
rs 2
c 0
b 0
f 0
wmc 229

13 Methods

Rating   Name   Duplication   Size   Complexity  
F beforeSendToClient() 0 144 40
B run_beforeSendToClient() 0 14 10
A __construct() 0 4 2
F ajax_get_rows() 0 263 54
F call_get_rows() 0 135 32
B get_timestamps() 0 32 10
A category_hierarchy() 0 35 6
F egw_actions() 0 156 40
B run() 0 28 8
A category_action() 0 35 6
A merge_actions_by_group() 0 28 6
A refresh() 0 5 1
C validate() 0 79 14

How to fix   Complexity   

Complex Class

Complex classes like Nextmatch often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Nextmatch, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * EGroupware - eTemplate serverside implementation of the nextmatch widget
4
 *
5
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
6
 * @package api
7
 * @subpackage etemplate
8
 * @link http://www.egroupware.org
9
 * @author Ralf Becker <[email protected]>
10
 * @copyright 2002-19 by [email protected]
11
 */
12
13
namespace EGroupware\Api\Etemplate\Widget;
14
15
use EGroupware\Api\Etemplate;
16
use EGroupware\Api;
17
18
/**
19
 * eTemplate serverside implementation of the nextmatch widget
20
 *
21
 * $content[$id] = array(	// I = value set by the app, 0 = value on return / output
22
 * 	'get_rows'       =>		// I  method/callback to request the data for the rows eg. 'notes.bo.get_rows'
23
 * 	'cat_id_label'   =>		// I  label for category  (optional)
24
 * 	'filter_label'   =>		// I  label for filter    (optional)
25
 * 	'filter2_label'   =>	// I  label for filter2   (optional)
26
 * 	'filter_help'    =>		// I  help-msg for filter (optional)
27
 * 	'no_filter'      => True// I  disable the 1. filter
28
 * 	'no_filter2'     => True// I  disable the 2. filter (params are the same as for filter)
29
 * 	'no_cat'         => True// I  disable the cat-selectbox
30
 *	'cat_app'        =>     // I  application the cat's should be from, default app in get_rows
31
 *	'cat_is_select'  =>     // I  true||'no_lang' use selectbox instead of category selection, default null
32
 * 	'template'       =>		// I  template to use for the rows, if not set via options
33
 * 	'header_left'    =>		// I  template to show left of the range-value, left-aligned (optional)
34
 * 	'header_right'   =>		// I  template to show right of the range-value, right-aligned (optional)
35
 * 	'bottom_too'     => True// I  show the nextmatch-line (arrows, filters, search, ...) again after the rows
36
 *	'never_hide'     => True// I  never hide the nextmatch-line if less then maxmatch entries
37
 *	'lettersearch'   => True// I  show a lettersearch
38
 *	'searchletter'   =>     // IO active letter of the lettersearch or false for [all]
39
 * 	'start'          =>		// IO position in list
40
 *	'num_rows'       =>     // IO number of rows to show, defaults to maxmatches from the general prefs
41
 * 	'cat_id'         =>		// IO category, if not 'no_cat' => True
42
 * 	'search'         =>		// IO search pattern
43
 * 	'order'          =>		// IO name of the column to sort after (optional for the sortheaders)
44
 * 	'sort'           =>		// IO direction of the sort: 'ASC' or 'DESC'
45
 * 	'col_filter'     =>		// IO array of column-name value pairs (optional for the filterheaders)
46
 * 							// grid requires implementation of folowing filters in get_rows, even if not used as regular filters!
47
 * 							//  O col_filter[$row_id]   to query certain rows only
48
 * 							//  O col_filter[$parent_id] row_id of parent to query children for hierachical display
49
 * 	'filter'         =>		// IO filter, if not 'no_filter' => True
50
 * 	'filter_no_lang' => True// I  set no_lang for filter (=dont translate the options)
51
 *	'filter_onchange'=> 'this.form.submit();' // I onChange action for filter, default: this.form.submit();
52
 * 	'filter2'        =>		// IO filter2, if not 'no_filter2' => True
53
 * 	'filter2_no_lang'=> True// I  set no_lang for filter2 (=dont translate the options)
54
 *	'filter2_onchange'=> 'this.form.submit();' // I onChange action for filter2, default: this.form.submit();
55
 * 	'rows'           =>		//  O content set by callback
56
 * 	'total'          =>		//  O the total number of entries
57
 * 	'sel_options'    =>		//  O additional or changed sel_options set by the callback and merged into $tmpl->sel_options
58
 * 	'no_columnselection' => // I  turns off the columnselection completly, turned on by default
59
 * 	'columnselection_pref' => // I  name of the preference (plus 'nextmatch-' prefix), default = template-name
60
 * 	'default_cols'   => 	// I  columns to use if there's no user or default pref (! as first char uses all but the named columns), default all columns
61
 * 	'options-selectcols' => // I  array with name/label pairs for the column-selection, this gets autodetected by default. A name => false suppresses a column completly.
62
 *	'return'         =>     // IO allows to return something from the get_rows function if $query is a var-param!
63
 *	'csv_fields'     =>		// I  false=disable csv export, true or unset=enable it with auto-detected fieldnames or preferred importexport definition,
64
 * 		array with name=>label or name=>array('label'=>label,'type'=>type) pairs (type is a eT widget-type)
65
 *		or name of import/export definition
66
 *  'row_id'         =>     // I  key into row content to set it's value as row-id, eg. 'id'
67
 *  'row_modified'   =>		// I  key into row content for modification date or state of a row, to not query it again
68
 *  'parent_id'      =>		// I  key into row content of children linking them to their parent, also used as col_filter to query children
69
 *  'is_parent'      =>		// I  key into row content to mark a row to have children
70
 *  'is_parent_value'=>     // I  if set value of is_parent, otherwise is_parent is evaluated as boolean
71
 *  'dataStorePrefix'	=>	// I Optional prefix for client side cache to prevent collisions in applications that have more than one data set, such as ProjectManager / Project elements.  Defaults to appname if not set.
72
 *  'actions'        =>     // I  array with actions, see nextmatch_widget::egw_actions
73
 *  'action_links'   =>     // I  array with enabled actions or ones which should be checked if they are enabled
74
 *                                optional, default id of all first level actions plus the ones with enabled='javaScript:...'
75
 *  'action_var'     => 'action'	// I name of var to return choosen action, default 'action'
76
 *  'action'         =>     //  O string selected action
77
 *  'selected'       =>     //  O array with selected id's
78
 *  'checkboxes'     =>     //  O array with checkbox id as key and boolean checked value
79
 *  'select_all'     =>     //  O boolean value of select_all checkbox, reference to above value for key 'select_all'
80
 *  'favorites'      =>     //  I boolean|array True to enable favorites, or an array of additional, app specific settings to include
81
 *					in the saved filters (eg: pm_id)
82
 *  'placeholder'    =>     //  I String Optional text to display in the empty row placeholder.  If not provided, it's "No matches found."
83
 *  'placeholder_actions' =>     //  I Array Optional list of actions allowed on the placeholder.  If not provided, it's ["add"].
84
 */
85
class Nextmatch extends Etemplate\Widget
86
{
87
	/**
88
	 * Path where the icons are stored (relative to webserver_url)
89
	 */
90
	const ICON_PATH = '/api/images';
91
92
	public function __construct($xml='')
93
	{
94
		if($xml) {
95
			parent::__construct($xml);
96
		}
97
	}
98
99
	/**
100
	 * Legacy options
101
	 */
102
	protected $legacy_options = 'template';
103
104
	/**
105
	 * Number of rows to send initially
106
	 */
107
	const INITIAL_ROWS = 50;
108
109
	/**
110
	 * Set up what we know on the server side.
111
	 *
112
	 * Sending a first chunk of rows
113
	 *
114
	 * @param string $cname
115
	 * @param array $expand values for keys 'c', 'row', 'c_', 'row_', 'cont'
116
	 */
117
	public function beforeSendToClient($cname, array $expand=null)
118
	{
119
		$form_name = self::form_name($cname, $this->id, $expand);
120
		$value = self::get_array(self::$request->content, $form_name, true);
121
122
		$value['start'] = 0;
123
		if(!array_key_exists('num_rows',$value))
124
		{
125
			$value['num_rows'] = self::INITIAL_ROWS;
126
		}
127
128
		$value['rows'] = array();
129
130
		$send_value = $value;
131
132
		list($app) = explode('.',$value['get_rows']);
133
		if(!$GLOBALS['egw_info']['apps'][$app])
134
		{
135
			list($app) = explode('.',$this->attrs['template']);
136
		}
137
138
		// Check for a favorite in URL
139
		if($_GET['favorite'] && $value['favorites'])
140
		{
141
			$safe_name = preg_replace('/[^A-Za-z0-9-_]/','_',strip_tags($_GET['favorite']));
142
			$pref_name = "favorite_" .$safe_name;
143
144
			// Do some easy applying of filters server side
145
			$favorite = $GLOBALS['egw_info']['user']['preferences'][$app][$pref_name];
146
			if(!$favorite && $_GET['favorite'] == 'blank')
147
			{
148
				// Have to go through each of these
149
				foreach(array('search','cat_id','filter','filter2') as $filter)
150
				{
151
					$send_value[$filter] = '';
152
				}
153
				unset($send_value['col_filter']);
154
			}
155
			// Old type
156
			if($favorite && $favorite['filter'])
157
			{
158
				$favorite['state'] = $favorite['filter'];
159
			}
160
			if($favorite && $favorite['state'])
161
			{
162
				$send_value = array_merge($value, $favorite['state']);
163
164
				// Ajax call can handle the saved sort here, but this can't
165
				if($favorite['state']['sort'])
166
				{
167
					unset($send_value['sort']);
168
					$send_value['order'] = $favorite['state']['sort']['id'];
169
					$send_value['sort'] = $favorite['state']['sort']['asc'] ? 'ASC' : 'DESC';
170
				}
171
			}
172
		}
173
		// Make sure it's not set
174
		unset($send_value['favorite']);
175
176
		// Parse sort into something that get_rows functions are expecting: db_field in order, ASC/DESC in sort
177
		if(is_array($send_value['sort']))
178
		{
179
			$send_value['order'] = $send_value['sort']['id'];
180
			$send_value['sort'] = $send_value['sort']['asc'] ? 'ASC' : 'DESC';
181
		}
182
		if($value['num_rows'] != 0)
183
		{
184
			$total = self::call_get_rows($send_value, $send_value['rows'], self::$request->readonlys, null, null, $this);
185
		}
186
		if (true) $value =& self::get_array(self::$request->content, $form_name, true);
187
188
		// Add favorite here so app doesn't save it in the session
189
		if($_GET['favorite'])
190
		{
191
			$send_value['favorite'] = $safe_name;
192
		}
193
		if (true) $value = $send_value;
194
		$value['total'] = $total;
195
196
		// Send categories
197
		if(!$value['no_cat'] && !$value['cat_is_select'])
198
		{
199
			$cat_app = $value['cat_app'] ? $value['cat_app'] : $GLOBALS['egw_info']['flags']['current_app'];
200
			$value['options-cat_id'] = self::$request->sel_options['cat_id'] ? self::$request->sel_options['cat_id'] : array();
201
202
			// Add 'All', if not already there
203
			if(!$value['options-cat_id'][''] && !$value['options-cat_id'][0])
204
			{
205
				$value['options-cat_id'][''] = lang('All categories');
206
			}
207
			$value['options-cat_id'] += Select::typeOptions('select-cat', ',,'.$cat_app,$no_lang=true,false,$value['cat_id']);
0 ignored issues
show
Bug introduced by
$no_lang = true cannot be passed to EGroupware\Api\Etemplate...t\Select::typeOptions() as the parameter $no_lang 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

207
			$value['options-cat_id'] += Select::typeOptions('select-cat', ',,'.$cat_app,/** @scrutinizer ignore-type */ $no_lang=true,false,$value['cat_id']);
Loading history...
208
			Select::fix_encoded_options($value['options-cat_id']);
209
		}
210
211
		// Favorite group for admins
212
		if($GLOBALS['egw_info']['apps']['admin'] && $value['favorites'])
213
		{
214
			self::$request->sel_options[$form_name]['favorite']['group'] = array('all' => lang('All users')) +
215
				Select::typeOptions('select-account',',groups');
216
		}
217
		foreach($value as $name => &$_value)
218
		{
219
			if(strpos($name, 'options-') !== false && $_value)
220
			{
221
				$select = substr($name, 8);
222
				if(!self::$request->sel_options[$select])
223
				{
224
					self::$request->sel_options[$select] = array();
225
				}
226
				Select::fix_encoded_options($_value, TRUE);
227
				self::$request->sel_options[$select] += $_value;
228
				// The client doesn't need them in content, but we can't unset them because
229
				// some apps don't send them on re-load, pulling them from the session
230
				//unset($value[$name]);
231
			}
232
		}
233
		if($value['rows']['sel_options'])
234
		{
235
			self::$request->sel_options = array_merge(self::$request->sel_options,$value['rows']['sel_options']);
236
			unset($value['rows']['sel_options']);
237
		}
238
239
		// If column selection preference is forced, set a flag to turn off UI
240
		$pref_name = 'nextmatch-' . (isset($value['columnselection_pref']) ? $value['columnselection_pref'] : $this->attrs['template']);
241
		$value['no_columnselection'] = $value['no_columnselection'] || (
242
			$GLOBALS['egw']->preferences->forced[$app][$pref_name] &&
243
			// Need to check admin too, or it will be impossible to turn off
244
			!$GLOBALS['egw_info']['user']['apps']['admin']
245
		);
246
		// Use this flag to indicate to the admin that columns are forced (and that's why they can't change)
247
		$value['columns_forced'] = (boolean)$GLOBALS['egw']->preferences->forced[$app][$pref_name];
248
249
		// todo: no need to store rows in request, it's enought to send them to client
250
251
		//error_log(__METHOD__."() $this: total=$value[total]");
252
		//foreach($value['rows'] as $n => $row) error_log("$n: ".array2string($row));
253
254
		// set up actions, but only if they are defined AND not already set up (run throught self::egw_actions())
255
		if (isset($value['actions']) && !isset($value['actions'][0]))
256
		{
257
			$value['action_links'] = array();
258
			$template_name = isset($value['template']) ? $value['template'] : $this->attrs['options'];
259
			if (!is_array($value['action_links'])) $value['action_links'] = array();
260
			$value['actions'] = self::egw_actions($value['actions'], $template_name, '', $value['action_links']);
261
		}
262
	}
263
264
	/**
265
	 * Callback to fetch more rows
266
	 *
267
	 * Callback uses existing get_rows callback, but requires now 'row_id' to be set.
268
	 * If no 'row_modified' is set, rows cant checked for modification and therefore
269
	 * are always returned to client if in range or deleted if outside range.
270
	 *
271
	 * @param string $exec_id identifys the etemplate request
272
	 * @param array $queriedRange array with values for keys "start", "num_rows" and optional "refresh", "parent_id"
273
	 * @param array $filters Search and filter parameters, passed to data source
274
	 * @param string $form_name ='nm' full id of widget incl. all namespaces
275
	 * @param array $knownUids =null uid's know to client
276
	 * @param int $lastModified =null date $knowUids last checked
277
	 * @todo for $queriedRange[refresh] first check if there's any modification since $lastModified, return $result[order]===null
278
	 * @return array with values for keys 'total', 'rows', 'readonlys', 'order', 'data' and 'lastModification'
279
	 */
280
	static public function ajax_get_rows($exec_id, array $queriedRange, array $filters = array(), $form_name='nm',
281
		array $knownUids=null, $lastModified=null)
282
	{
283
		self::$request = Etemplate\Request::read($exec_id);
0 ignored issues
show
Documentation Bug introduced by
It seems like EGroupware\Api\Etemplate\Request::read($exec_id) can also be of type EGroupware\Api\Etemplate\Request. However, the property $request is declared as type EGroupware\Api\Etemplate\etemplate_request. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
284
		// fix for somehow empty etemplate request content
285
		if (!is_array(self::$request->content))
286
		{
287
			self::$request->content = array($form_name => array());
288
		}
289
		self::$response = Api\Json\Response::get();
290
291
		$value = self::get_array(self::$request->content, $form_name, true);
292
		if(!is_array($value))
293
		{
294
			$value = ($value) ? array($value) : array();
295
		}
296
297
		// Validate filters
298
		if (($template = Template::instance(self::$request->template['name'], self::$request->template['template_set'],
299
			self::$request->template['version'], self::$request->template['load_via'])))
300
		{
301
			$template = $template->getElementById($form_name, strpos($form_name, 'history') === 0 ? 'historylog' : 'nextmatch');
302
			$expand = array(
303
				'cont' => array($form_name => $filters),
304
			);
305
			$valid_filters = array();
306
307
			if($template)
308
			{
309
				$template->run('validate', array('', $expand, $expand['cont'], &$valid_filters), false);	// $respect_disabled=false: as client may disable things, here we validate everything and leave it to the get_rows to interpret
310
				$filters = $valid_filters[$form_name];
311
			}
312
			// Avoid empty arrays, they cause problems with db filtering
313
			foreach((array)$filters['col_filter'] as $col => $val)
314
			{
315
				if(is_array($val) && count($val) == 0)
316
				{
317
					unset($filters['col_filter'][$col]);
318
				}
319
			}
320
			//error_log($this . " Valid filters: " . array2string($filters));
321
		}
322
		else
323
		{
324
			$template = null;	// get_rows method requires null, not false
325
		}
326
327
		if (true) $value = $value_in = array_merge($value, $filters);
328
329
		//error_log(__METHOD__."('".substr($exec_id,0,10)."...', range=".array2string($queriedRange).', filters='.array2string($filters).", '$form_name', knownUids=".array2string($knownUids).", lastModified=$lastModified) parent_id=$value[parent_id], is_parent=$value[is_parent]");
330
331
		$result = array();
332
333
		// Parse sort into something that get_rows functions are expecting: db_field in order, ASC/DESC in sort
334
		if(is_array($value['sort']))
335
		{
336
			$value['order'] = $value['sort']['id'];
337
			$value['sort'] = $value['sort']['asc'] ? 'ASC' : 'DESC';
338
		}
339
340
		$value['start'] = (int)$queriedRange['start'];
341
		$value['num_rows'] = (int)$queriedRange['num_rows'];
342
		if($value['num_rows'] == 0) $value['num_rows'] = self::INITIAL_ROWS;
343
		// if app supports parent_id / hierarchy ($value['parent_id'] not empty), set parent_id as filter
344
		if (($parent_id = $value['parent_id']))
345
		{
346
			// Infolog at least wants 'parent_id' instead of $parent_id
347
			$value['col_filter'][$parent_id] = $queriedRange['parent_id'];
348
			if ($queriedRange['parent_id']) $value['csv_export'] = 'children';
349
		}
350
351
		// Set current app for get_rows
352
		list($app) = explode('.',self::$request->method);
353
		if(!$app) list($app) = explode('::',self::$request->method);
354
		if($app)
355
		{
356
			$GLOBALS['egw_info']['flags']['currentapp'] = $app;
357
			Api\Translation::add_app($app);
358
		}
359
		// If specific data requested, just do that
360
		if (($row_id = $value['row_id']) && $queriedRange['refresh'])
361
		{
362
			$value['col_filter'][$row_id] = $queriedRange['refresh'];
363
			$value['csv_export'] = 'refresh';
364
		}
365
		$rows = $result['data'] = $result['order'] = array();
366
		$result['total'] = self::call_get_rows($value, $rows, $result['readonlys'], null, null, $template);
367
		$result['lastModification'] = Api\DateTime::to('now', 'ts')-1;
368
369
		if (isset($GLOBALS['egw_info']['flags']['app_header']) && self::$request->app_header != $GLOBALS['egw_info']['flags']['app_header'])
370
		{
371
			self::$request->app_header = $GLOBALS['egw_info']['flags']['app_header'];
372
			Api\Json\Response::get()->apply('egw_app_header', array($GLOBALS['egw_info']['flags']['app_header']));
373
		}
374
375
		$GLOBALS['egw']->session->commit_session();
376
377
		$row_id = isset($value['row_id']) ? $value['row_id'] : 'id';
378
		$row_modified = $value['row_modified'];
379
380
		foreach($rows as $n => $row)
381
		{
382
			$kUkey = false;
383
			if (is_int($n) && $row)
384
			{
385
				if (!isset($row[$row_id])) unset($row_id);	// unset default row_id of 'id', if not used
386
				if (!isset($row[$row_modified])) unset($row_modified);
387
388
				$id = $row_id ? $row[$row_id] : $n;
389
				$result['order'][] = $id;
390
391
				$modified = $row[$row_modified];
392
				if (isset($modified) && !(is_int($modified) || is_string($modified) && is_numeric($modified)))
393
				{
394
					$modified = Api\DateTime::to(str_replace('Z', '', $modified), 'ts');
395
				}
396
397
				// check if we need to send the data
398
				//error_log("$id Known: " . (array_search($id, $knownUids) !== false ? 'Yes' : 'No') . ' Modified: ' . Api\DateTime::to($row[$row_modified]) . ' > ' . Api\DateTime::to($lastModified).'? ' . ($row[$row_modified] > $lastModified ? 'Yes' : 'No'));
399
				if (!$row_id || !$knownUids || ($kUkey = array_search($id, $knownUids)) === false ||
400
					!$lastModified || !isset($modified) || $modified > $lastModified ||
401
					$queriedRange['refresh'] && $id == $queriedRange['refresh']
402
				)
403
				{
404
					$result['data'][$id] = $row;
405
				}
406
407
				if ($kUkey !== false) unset($knownUids[$kUkey]);
408
			}
409
			else	// non-row data set by get_rows method
410
			{
411
				// Encode all select options and re-index to avoid Firefox's problem with
412
				// '' => 'All'
413
				if($n == 'sel_options')
414
				{
415
					foreach($row as &$options)
416
					{
417
						Select::fix_encoded_options($options,true);
418
					}
419
				}
420
				$result['rows'][$n] = $row;
421
			}
422
		}
423
		// check knowUids outside of range for modification - includes deleted
424
		/*
425
		if ($knownUids)
426
		{
427
			// row_id not set for nextmatch --> just skip them, we can't identify the rows
428
			if (!$row_id)
429
			{
430
				foreach($knownUids as $uid)
431
				{
432
					// Just don't send it back for now
433
					unset($result['data'][$uid]);
434
					//$result['data'][$uid] = null;
435
				}
436
			}
437
			else
438
			{
439
				error_log(__METHOD__."() knowUids left to check ".array2string($knownUids));
440
				// check if they are up to date: we create a query similar to csv-export without any filters
441
				$uid_query = $value;
442
				$uid_query['csv_export'] = 'knownUids';	// do not store $value in session
443
				$uid_query['filter'] = $uid_query['filter2'] = $uid_query['cat_id'] = $uid_query['search'] = '';
444
				$uid_query['col_filter'] = array($row_id => $knownUids);
445
				// if we know name of modification column and have a last-modified date
446
				if ($row_modified && $lastModified)	// --> set filter to return only modified entries
447
				{
448
					$uid_query['col_filter'][] = $row_modified.' > '.(int)$lastModified;
449
				}
450
				$uid_query['start'] = 0;
451
				$uid_query['num_rows'] = count($knownUids);
452
				$rows = array();
453
				try
454
				{
455
					if (self::call_get_rows($uid_query, $rows))
456
					{
457
						foreach($rows as $n => $row)
458
						{
459
							if (!is_int($n)) continue;	// ignore non-row data set by get_rows method
460
461
							if (!$row_modified || !isset($row[$row_modified]) ||
462
								!isset($lastModified) || $row[$row_modified] > $lastModified)
463
							{
464
								$result['data'][$row[$row_id]] = $row;
465
								$kUkey = array_search($id, $knownUids);
466
								if ($kUkey !== false) unset($knownUids[$kUkey]);
467
							}
468
						}
469
					}
470
				}
471
				catch (Exception $e)
472
				{
473
					unset($value['row_modified']);
474
					error_log("Error trying to find changed rows with {$value['get_rows']}, falling back to all rows. ");
475
					error_log($e);
476
				}
477
478
				// Remove any remaining knownUIDs from the grid
479
				foreach($knownUids as $uid)
480
				{
481
					$result['data'][$uid] = null;
482
				}
483
			}
484
		}
485
		 */
486
487
		// Check for anything changed in the query
488
		// Tell the client about the changes
489
		$request_value =& self::get_array(self::$request->content, $form_name,true);
490
		$changes = $no_rows = false;
0 ignored issues
show
Unused Code introduced by
The assignment to $no_rows is dead and can be removed.
Loading history...
491
492
		foreach(array_keys($value_in) + array_keys($value) as $key)
493
		{
494
			// These keys are ignored
495
			if(in_array($key, array('col_filter','start','num_rows','total','order','sort')))
496
			{
497
				continue;
498
			}
499
			if($value_in[$key] == $value[$key]) continue;
500
501
			// These keys we don't send row data back, as they cause a partial reload
502
			if(in_array($key, array('template'))) $no_rows = true;
503
504
			// Actions still need extra handling
505
			if($key == 'actions' && !isset($value['actions'][0]))
506
			{
507
				$value['action_links'] = array();
508
				$template_name = isset($value['template']) ? $value['template'] : '';
509
				if (!is_array($value['action_links'])) $value['action_links'] = array();
510
				$value['actions'] = self::egw_actions($value['actions'], $template_name, '', $value['action_links']);
511
			}
512
513
			$changes = true;
514
			$request_value[$key] = $value[$key];
515
516
			Api\Json\Response::get()->generic('assign', array(
517
				'etemplate_exec_id' => $exec_id,
518
				'id' => $form_name,
519
				'key' => $key,
520
				'value' => $value[$key],
521
			));
522
		}
523
		// Request doesn't handle changing by reference, so force it
524
		if($changes)
525
		{
526
			$content = self::$request->content;
527
			self::$request->content = array();
528
			self::$request->content = $content;
529
		}
530
531
		// Send back data
532
		//foreach($result as $name => $value) if ($name != 'readonlys') error_log(__METHOD__."() result['$name']=".array2string($name == 'data' ? array_keys($value) : $value));
533
		Api\Json\Response::get()->data($result);
534
535
		// If etemplate_exec_id has changed, update the client side
536
		if (($new_id = self::$request->id()) != $exec_id)
537
		{
538
			Api\Json\Response::get()->generic('assign', array(
539
				'etemplate_exec_id' => $exec_id,
540
				'id' => '',
541
				'key' => 'etemplate_exec_id',
542
				'value' => $new_id,
543
			));
544
		}
545
	}
546
547
	/**
548
	 * Calling our callback
549
	 *
550
	 * Signature of get_rows callback is either:
551
	 * a) int get_rows($query,&$rows,&$readonlys)
552
	 * b) int get_rows(&$query,&$rows,&$readonlys)
553
	 *
554
	 * If get_rows is called static (and php >= 5.2.3), it is always b) independent on how it's defined!
555
	 *
556
	 * @param array &$value
557
	 * @param array &$rows on return: rows are indexed by their row-number: $value[start], ..., $value[start]+$value[num_rows]-1
558
	 * @param array &$readonlys =null
559
	 * @param object $obj =null (internal)
560
	 * @param string|array $method =null (internal)
561
	 * @param Etemplate\Widget $widget =null instanciated nextmatch widget to let it's widgets transform each row
562
	 * @return int|boolean total items found of false on error ($value['get_rows'] not callable)
563
	 */
564
	private static function call_get_rows(array &$value,array &$rows,array &$readonlys=null,$obj=null,$method=null, Etemplate\Widget $widget=null)
565
	{
566
		if (is_null($method)) $method = $value['get_rows'];
567
568
		if (is_null($obj))
569
		{
570
			// allow static callbacks
571
			if(strpos($method,'::') !== false)
572
			{
573
				list($class,$method) = explode('::',$method);
574
575
				//  workaround for php < 5.2.3: do NOT call it static, but allow application code to specify static callbacks
576
				if (version_compare(PHP_VERSION,'5.2.3','>='))
577
				{
578
					$method = array($class,$method);
579
					unset($class);
580
				}
581
			}
582
			else
583
			{
584
				list($app,$class,$method) = explode('.',$value['get_rows']);
585
			}
586
			if ($class)
587
			{
588
				if (!$app && !is_object($GLOBALS[$class]))
589
				{
590
					$GLOBALS[$class] = new $class();
591
				}
592
				if (is_object($GLOBALS[$class]))	// use existing instance (put there by a previous CreateObject)
593
				{
594
					$obj = $GLOBALS[$class];
595
				}
596
				else
597
				{
598
					$obj = CreateObject($app.'.'.$class);
0 ignored issues
show
Deprecated Code introduced by
The function CreateObject() has been deprecated: use autoloadable class-names and new ( Ignorable by Annotation )

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

598
					$obj = /** @scrutinizer ignore-deprecated */ CreateObject($app.'.'.$class);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
599
				}
600
			}
601
		}
602
		$raw_rows = array();
603
		if (!is_array($readonlys)) $readonlys = array();
604
		if(is_callable($method))	// php5.2.3+ static call (value is always a var param!)
605
		{
606
			$total = call_user_func_array($method,array(&$value,&$raw_rows,&$readonlys));
607
		}
608
		elseif(is_object($obj) && method_exists($obj,$method))
609
		{
610
			$total = $obj->$method($value,$raw_rows,$readonlys);
611
		}
612
		else
613
		{
614
			$total = false;	// method not callable
615
		}
616
617
		// allow to hook into get_rows of other apps
618
		Api\Hooks::process(array(
619
			'hook_location' => 'etemplate2_after_get_rows',
620
			'get_rows'      => $method,
621
			'value'         => &$value,
622
			'rows'          => &$rows,
623
			'readonlys'     => &$readonlys,
624
			'total'         => &$total,
625
		), array(), true);	// true = no permission check
626
627
		// if we have a nextmatch widget, find the repeating row
628
		if ($widget && $widget->attrs['template'])
629
		{
630
			$row_template = $widget->getElementById($widget->attrs['template']);
631
			if(!$row_template)
632
			{
633
				$row_template = Template::instance($widget->attrs['template']);
634
			}
635
636
			// Try to find just the repeating part
637
			$repeating_row = null;
638
			// First child should be a grid, we want last row
639
			foreach($row_template->children[0]->children[1]->children as $child)
640
			{
641
				if($child->type == 'row') $repeating_row = $child;
642
			}
643
		}
644
		// otherwise we might get stoped by max_excutiontime
645
		if ($total > 200) @set_time_limit(0);
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

645
		if ($total > 200) /** @scrutinizer ignore-unhandled */ @set_time_limit(0);

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...
646
647
		$is_parent = $value['is_parent'];
648
		$is_parent_value = $value['is_parent_value'];
649
		$parent_id = $value['parent_id'];
650
651
		// remove empty rows required by old etemplate to compensate for header rows
652
		$first = $total ? null : 0;
653
		foreach($raw_rows as $n => $row)
654
		{
655
			// skip empty rows inserted for each header-line in old etemplate
656
			if (is_int($n) && is_array($rows))
657
			{
658
				if (is_null($first)) $first = $n;
659
660
				if ($row[$is_parent])	// if app supports parent_id / hierarchy, set parent_id and is_parent
661
				{
662
					$row['is_parent'] = isset($is_parent_value) ?
663
						$row[$is_parent] == $is_parent_value : (boolean)$row[$is_parent];
664
					$row['parent_id'] = $row[$parent_id];	// seems NOT used on client!
665
				}
666
				// run beforeSendToClient methods of widgets in row on row-data
667
				if($repeating_row)
668
				{
669
					// Change anything by widget for each row ($row set to 1)
670
					$_row = array(1 => &$row);
671
					$repeating_row->run('set_row_value', array('',array('row' => 1), &$_row), true);
672
				}
673
				else if (!$widget || get_class($widget) != __NAMESPACE__.'\\HistoryLog')
674
				{
675
					// Fallback based on widget names
676
					//error_log(self::$request->template['name'] . ' had to fallback to run_beforeSendToClient() because it could not find the row');
677
					$row = self::run_beforeSendToClient($row);
678
				}
679
				$rows[$n-$first+$value['start']] = $row;
680
			}
681
			elseif(!is_numeric($n))	// rows with string-keys, after numeric rows
682
			{
683
				if($n == 'sel_options')
684
				{
685
					foreach($row as $name => &$options)
686
					{
687
						// remember newly set options for validation of nextmatch filters
688
						self::$request->sel_options[$name] = $options;
689
690
						Select::fix_encoded_options($options, true);
691
					}
692
				}
693
				$rows[$n] = $row;
694
			}
695
		}
696
697
		//error_log($value['get_rows'].'() returning '.array2string($total).', method = '.array2string($method).', value = '.array2string($value));
698
		return $total;
699
	}
700
701
	/**
702
	 * Run beforeSendToClient methods of widgets in row over row-data
703
	 *
704
	 * This is currently only a hack to convert everything looking like a timestamp to a 'Y-m-d\TH:i:s\Z' string, fix timezone problems!
705
	 *
706
	 * @todo instanciate row of template and run it's beforeSendToClient
707
	 * @param array $row
708
	 * @return array
709
	 */
710
	private static function run_beforeSendToClient(array $row)
711
	{
712
		$timestamps = self::get_timestamps();
713
714
		foreach($row as $name => &$value)
715
		{
716
			if ($name[0] != '#' && in_array($name, $timestamps) && $value &&
717
				(is_int($value) || is_string($value) && is_numeric($value)) &&
718
				($value > 21000000 || $value < 19000000))
719
			{
720
				$value = Api\DateTime::to($value, 'Y-m-d\TH:i:s\Z');
721
			}
722
		}
723
		return $row;
724
	}
725
726
	/**
727
	 * Get all timestamp columns incl. names with removed prefixes like cal_ or contact_
728
	 *
729
	 * @return array
730
	 */
731
	private static function get_timestamps()
732
	{
733
		return Api\Cache::getTree(__CLASS__, 'timestamps', function()
734
		{
735
			$timestamps = array();
736
			foreach(scandir(EGW_SERVER_ROOT) as $app)
737
			{
738
				$dir = EGW_SERVER_ROOT.'/'.$app;
739
				if (is_dir($dir) && file_exists($dir.'/setup/tables_current.inc.php') &&
740
					($tables_defs = $GLOBALS['egw']->db->get_table_definitions($app)))
741
				{
742
					foreach($tables_defs as $defintion)
743
					{
744
						foreach($defintion['fd'] as $col => $data)
745
						{
746
							if ($data['type'] == 'timestamp' || $data['meta'] == 'timestamp')
747
							{
748
								$timestamps[] = $col;
749
								// some apps remove a prefix --> add prefix-less version too
750
								$matches = null;
751
								if (preg_match('/^(tz|acl|async|cal|contact|lock|history|link|cf|cat|et)_(.+)$/', $col, $matches))
752
								{
753
									$timestamps[] = $matches[2];
754
								}
755
							}
756
						}
757
					}
758
				}
759
			}
760
			//error_log(__METHOD__."() returning ".array2string($timestamps));
761
			return $timestamps;
762
		}, array(), 86400);	// cache for 1 day
763
	}
764
765
	/**
766
	 * Merges actions together with positions based on group parameter
767
	 *
768
	 * @param array $actions
769
	 * @param array $actions2
770
	 * @return array
771
	 */
772
	static function merge_actions_by_group(array $actions, array $actions2)
773
	{
774
		//error_log(__METHOD__.'('.array2string($actions).', '.array2string($actions2).')');
775
776
		//return array_merge_recursive($actions, $actions2);
777
		foreach($actions2 as $name => $action)
778
		{
779
			// overwrite existing action of given name or append action without group
780
			if (isset($actions[$name]) || !isset($action['group']))
781
			{
782
				$actions[$name] = $action;
783
			}
784
			// find position to insert action
785
			else
786
			{
787
				$n = 0;
788
				foreach($actions as $a)
789
				{
790
					if ($a['group'] > $action['group']) break;
791
					++$n;
792
				}
793
				$actions = array_merge(array_slice($actions, 0, $n),
794
					array($name => $action),
795
					array_slice($actions, $n, count($actions)-$n));
796
			}
797
		}
798
		//error_log(__METHOD__.'() returning '.array2string($actions));
799
		return $actions;
800
	}
801
802
	/**
803
	 * Default maximum length for context submenus, longer menus are put as a "More" submenu
804
	 */
805
	const DEFAULT_MAX_MENU_LENGTH = 14;
806
807
	/**
808
	 * Return egw_actions
809
	 *
810
	 * The following attributes are understood for actions on eTemplate/PHP side:
811
	 * - string 'id' id of the action (set as key not attribute!)
812
	 * - string 'caption' name/label or action, get's automatic translated
813
	 * - boolean 'no_lang' do NOT translate caption, default false
814
	 * - string 'icon' icon, eg. 'edit' or 'infolog/task', if no app given app of template or API is used
815
	 * - string 'iconUrl' full url of icon, better use 'icon'
816
	 * - boolean|string 'allowOnMultiple' should action be shown if multiple lines are marked, or string 'only', default true!
817
	 * - boolean|string 'enabled' is action available, or string with javascript function to call, default true!
818
	 * - string 'disableClass' class name to check if action should be disabled (if presend, enabled if not)
819
	 *   (add that css class in get_rows(), if row lacks rights for an action)
820
	 * - string 'enableClass' class name to check if action should be enabled (if present, disabled if not)
821
	 * - string 'enableId' regular expression row-id has to match to enable action
822
	 * - boolean 'hideOnDisabled' hide disabled actions, default false
823
	 * - string 'type' type of action, default 'popup' for contenxt menus, 'drag' or 'drop'
824
	 * - boolean 'default' is that action the default action, default false
825
	 * - array  'children' array with actions of submenu
826
	 * - int    'group' to group items, default all actions are in one group
827
	 * - string 'onExecute' javascript to run, default 'javaScript:nm_action' or eg. 'javaScript:app.myapp.someMethod'
828
	 *   which runs action specified in nm_action attribute:
829
	 * - string 'nm_action'
830
	 *   + 'alert'  debug action, shows alert with action caption, id and id's of selected rows
831
	 *   + 'submit' default action, sets nm[action], nm[selected] and nm[select_all]
832
	 *   + 'location' redirects / set location.href to 'url' attribute
833
	 *   + 'popup'  opens popup with url given in 'url' attribute
834
	 * - string 'url' url for location or popup
835
	 * - string 'target' target for location or popup
836
	 * - string 'width' for popup
837
	 * - string 'height' for popup
838
	 * - string 'confirm' confirmation message
839
	 * - string 'confirm_multiple' confirmation message for multiple selected, defaults to 'confirm'
840
	 * - boolean 'postSubmit' eg. downloads need a submit via POST request not our regular Ajax submit, only works with nm_action=submit!
841
	 * - string 'hint' tooltip on menu item
842
	 *
843
	 * @param array $actions id indexed array of actions / array with valus for keys: 'iconUrl', 'caption', 'onExecute', ...
844
	 * @param string $template_name ='' name of the template, used as default for app name of images
845
	 * @param string $prefix ='' prefix for ids
846
	 * @param array &$action_links =array() on return all first-level actions plus the ones with enabled='javaScript:...'
847
	 * @param int $max_length =self::DEFAULT_MAX_MENU_LENGTH automatic pagination, not for first menu level!
848
	 * @param array $default_attrs =null default attributes
849
	 * @return array
850
	 */
851
	public static function egw_actions(array $actions=null, $template_name='', $prefix='', array &$action_links=array(),
852
		$max_length=self::DEFAULT_MAX_MENU_LENGTH, array $default_attrs=null)
853
	{
854
		//echo "<p>".__METHOD__."(\$actions, '$template_name', '$prefix', \$action_links, $max_length) \$actions="; _debug_array($actions);
855
		$first_level = !$action_links;	// add all first level actions
856
857
		if ($first_level)
858
		{
859
			// allow other apps to add actions
860
			foreach(Api\Hooks::process(array(
861
				'location' => 'add_row_actions',
862
				'template_name' => $template_name,
863
			), array('policy'), true) as $app => $data)
864
			{
865
				// todo: place new items based on group
866
				if ($data) $actions = self::merge_actions_by_group((array)$actions, $data);
867
			}
868
		}
869
870
		//echo "actions="; _debug_array($actions);
871
		$egw_actions = array();
872
		$n = 1;
873
		$group = false;
874
875
		foreach((array)$actions as $id => $action)
876
		{
877
			if (!empty($action['hideOnMobile']) && Api\Header\UserAgent::mobile())
878
			{
879
				continue;	// no need to send this action to client, specially document actions can be huge
880
			}
881
			if (!empty($action['disableIfNoEPL']) && $action['disableIfNoEPL'] && !$GLOBALS['egw_info']['apps']['stylite'])
882
			{
883
				$action['enabled'] =
884
				$action['hideOnDisabled'] = false;
885
				$action['hint'] = Lang("This feature is only available in EPL version.");
886
			}
887
			else if(!empty($action['disableIfNoEPL']))
888
			{
889
				unset($action['disableIfNoEPL']);
890
			}
891
			// in case it's only selectbox  id => label pairs
892
			if (!is_array($action)) $action = array('caption' => $action);
893
			if ($default_attrs) $action += $default_attrs;
894
895
			// Add 'Select All' after first group
896
			if ($first_level && $group !== false && $action['group'] != $group && !$egw_actions[$prefix.'select_all'])
897
			{
898
899
				$egw_actions[$prefix.'select_all'] = array(
900
					'caption' => 'Select all',
901
					//'checkbox' => true,
902
					'hint' => 'Select all entries',
903
					'enabled' => true,
904
					'shortcut' => array(
905
						'keyCode'	=>	65, // A
906
						'ctrl'		=>	true,
907
						'caption'	=> lang('Ctrl').'+A'
908
					),
909
					'group' => $action['group'],
910
				);
911
				$action_links[] = $prefix.'select_all';
912
			}
913
			$group = $action['group'];
914
915
			if (!$first_level && $n == $max_length && count($actions) > $max_length)
916
			{
917
				$id = 'more_'.count($actions);	// we need a new unique id
918
				$action = array(
919
					'caption' => 'More',
920
					'prefix' => $prefix,
921
					// display rest of actions incl. current one as children
922
					'children' => array_slice($actions, $max_length-1, count($actions)-$max_length+1, true),
923
				);
924
				//echo "*** Inserting id=$prefix$id"; _debug_array($action);
925
				// we break at end of foreach loop, as rest of actions is already dealt with
926
				// by putting them as children
927
928
				// sets the default attributes to every children dataset
929
				if (is_array($action['children']))
930
				{
931
					foreach ($action['children'] as $key => $children)
932
					{
933
						// checks if children is a valid array and if the "$default_attrs" variable exists
934
 						if (is_array($children) && $default_attrs)
935
						{
936
							$action['children'][$key] += $default_attrs;
937
						}
938
					}
939
				}
940
			}
941
942
			// add all first level popup actions plus ones with enabled = 'javaScript:...' to action_links
943
			if ((!isset($action['type']) || in_array($action['type'],array('popup','drag','drop'))) &&	// popup is the default
944
				($first_level || substr($action['enabled'],0,11) == 'javaScript:'))
945
			{
946
				$action_links[] = $prefix.$id;
947
			}
948
949
			// add sub-menues
950
			if ($action['children'])
951
			{
952
				static $inherit_attrs = array('url','popup','nm_action','onExecute','type','egw_open','allowOnMultiple','confirm','confirm_multiple');
953
				$inherit_keys = array_flip($inherit_attrs);
954
				$action['children'] = self::egw_actions($action['children'], $template_name, $action['prefix'], $action_links, $max_length,
955
					array_intersect_key($action, $inherit_keys));
956
957
				unset($action['prefix']);
958
959
				// Allow default actions to keep their onExecute
960
				if($action['default']) unset($inherit_keys['onExecute']);
961
				$action = array_diff_key($action, $inherit_keys);
962
			}
963
964
			// link or popup action
965
			if ($action['url'])
966
			{
967
				$action['url'] = Api\Framework::link('/index.php',str_replace('$action',$id,$action['url']));
968
				if ($action['popup'])
969
				{
970
					list($action['data']['width'],$action['data']['height']) = explode('x',$action['popup']);
971
					unset($action['popup']);
972
					$action['data']['nm_action'] = 'popup';
973
				}
974
				else
975
				{
976
					$action['data']['nm_action'] = 'location';
977
					if(!$action['target'] && strpos($action['url'],'menuaction') > 0)
978
					{
979
						// It would be better if app set target, but we'll auto-detect if not
980
						list(,$menuaction) = explode('=',$action['url']);
981
						list($app) = explode('.',$menuaction);
982
						$action['data']['target'] = $app;
983
					}
984
				}
985
			}
986
			if ($action['egw_open'])
987
			{
988
				$action['data']['nm_action'] = 'egw_open';
989
			}
990
991
			$egw_actions[$prefix.$id] = $action;
992
993
			if (!$first_level && $n++ == $max_length) break;
994
		}
995
996
		// Make sure select all is in a group by itself
997
		foreach($egw_actions as $id => &$_action)
998
		{
999
			if($id == $prefix . 'select_all') continue;
1000
			if($_action['group'] >= $egw_actions[$prefix.'select_all']['group'] )
1001
			{
1002
				$egw_actions[$id]['group']+=1;
1003
			}
1004
		}
1005
		//echo "egw_actions="; _debug_array($egw_actions);
1006
		return $egw_actions;
1007
	}
1008
1009
	/**
1010
	 * Action with submenu for categories
1011
	 *
1012
	 * Automatic switch to hierarchical display, if more then $max_cats_flat=14 cats found.
1013
	 *
1014
	 * @param string $app
1015
	 * @param int $group =0 see self::egw_actions
1016
	 * @param string $caption ='Change category'
1017
	 * @param string $prefix ='cat_' prefix category id to get action id
1018
	 * @param boolean $globals =true application global categories too
1019
	 * @param int $parent_id =0 only returns cats of a certain parent
1020
	 * @param int $max_cats_flat =self::DEFAULT_MAX_MENU_LENGTH use hierarchical display if more cats
1021
	 * @return array like self::egw_actions
1022
	 */
1023
	public static function category_action($app, $group=0, $caption='Change category',
1024
		$prefix='cat_', $globals=true, $parent_id=0, $max_cats_flat=self::DEFAULT_MAX_MENU_LENGTH)
1025
	{
1026
		$cat = new Api\Categories(null,$app);
1027
		$cats = $cat->return_sorted_array($start=0, false, '', 'ASC', 'cat_name', $globals, $parent_id, true);
1028
1029
		// if more then max_length cats, switch automatically to hierarchical display
1030
		if (count($cats) > $max_cats_flat)
1031
		{
1032
			$cat_actions = self::category_hierarchy($cats, $prefix, $parent_id);
1033
		}
1034
		else	// flat, indented categories
1035
		{
1036
			$cat_actions = array();
1037
			foreach((array)$cats as $cat)
1038
			{
1039
				$name = str_repeat('&nbsp;',2*$cat['level']) . stripslashes($cat['name']);
1040
1041
				$cat_actions[$cat['id']] = array(
1042
					'caption' => $name,
1043
					'no_lang' => true,
1044
				);
1045
				// add category icon
1046
				if (is_array($cat['data']) && $cat['data']['icon'] && file_exists(EGW_SERVER_ROOT.self::ICON_PATH.'/'.basename($cat['data']['icon'])))
1047
				{
1048
					$cat_actions[$cat['id']]['iconUrl'] = $GLOBALS['egw_info']['server']['webserver_url'].self::ICON_PATH.'/'.$cat['data']['icon'];
1049
				}
1050
			}
1051
		}
1052
		return array(
1053
			'caption' => $caption,
1054
			'children' => $cat_actions,
1055
			'enabled' => (boolean)$cat_actions,
1056
			'group' => $group,
1057
			'prefix' => $prefix,
1058
		);
1059
	}
1060
1061
	/**
1062
	 * Return one level of the category hierarchy
1063
	 *
1064
	 * @param array $cats =null all cats if already read
1065
	 * @param string $prefix ='cat_' prefix category id to get action id
1066
	 * @param int $parent_id =0 only returns cats of a certain parent
1067
	 * @return array
1068
	 */
1069
	private static function category_hierarchy(array $cats, $prefix, $parent_id=0)
1070
	{
1071
		$cat_actions = array();
1072
		foreach($cats as $key => $cat)
1073
		{
1074
			// current hierarchy level
1075
			if ($cat['parent'] == $parent_id)
1076
			{
1077
				$name = stripslashes($cat['name']);
1078
1079
				$cat_actions[$cat['id']] = array(
1080
					'caption' => $name,
1081
					'no_lang' => true,
1082
					'prefix' => $prefix,
1083
				);
1084
				// add category icon
1085
				if ($cat['data']['icon'] && file_exists(EGW_SERVER_ROOT.self::ICON_PATH.'/'.basename($cat['data']['icon'])))
1086
				{
1087
					$cat_actions[$cat['id']]['iconUrl'] = $GLOBALS['egw_info']['server']['webserver_url'].self::ICON_PATH.'/'.$cat['data']['icon'];
1088
				}
1089
				unset($cats[$key]);
1090
			}
1091
			// direct children
1092
			elseif(isset($cat_actions[$cat['parent']]))
1093
			{
1094
				$cat_actions['sub_'.$cat['parent']] = $cat_actions[$cat['parent']];
1095
				// have to add category itself to children, to be able to select it!
1096
				$cat_actions[$cat['parent']]['group'] = -1;	// own group on top
1097
				$cat_actions['sub_'.$cat['parent']]['children'] = array(
1098
					$cat['parent'] => $cat_actions[$cat['parent']],
1099
				)+self::category_hierarchy($cats, $prefix, $cat['parent']);
1100
				unset($cat_actions[$cat['parent']]);
1101
			}
1102
		}
1103
		return $cat_actions;
1104
	}
1105
1106
	/**
1107
	 * Validate input
1108
	 *
1109
	 * Following attributes get checked:
1110
	 * - needed: value must NOT be empty
1111
	 * - min, max: int and float widget only
1112
	 * - maxlength: maximum length of string (longer strings get truncated to allowed size)
1113
	 * - preg: perl regular expression incl. delimiters (set by default for int, float and colorpicker)
1114
	 * - int and float get casted to their type
1115
	 *
1116
	 * @param string $cname current namespace
1117
	 * @param array $expand values for keys 'c', 'row', 'c_', 'row_', 'cont'
1118
	 * @param array $content
1119
	 * @param array &$validated =array() validated content
1120
	 */
1121
	public function validate($cname, array $expand, array $content, &$validated=array())
1122
	{
1123
		$form_name = self::form_name($cname, $this->id, $expand);
1124
		$value = self::get_array($content, $form_name);
1125
1126
		// Some (most) nextmatch settings are set in its value, not attributes, which aren't in
1127
		// $content.  Fetch them from the request, so we actually have them.
1128
		$content_value = self::get_array(self::$request->content, $form_name);
1129
1130
		list($app) = explode('.',$this->attrs['template']);
1131
1132
		unset($value['favorite']);
1133
1134
		// On client, rows does not get its own namespace, but all apps are expecting it
1135
		$value['rows'] = $value;
1136
1137
		// Legacy support - action popups were not properly namespaced
1138
		$preserve = self::get_array(self::$request->preserv, $form_name);
1139
		if($value[$preserve['action_var']] && $content[$value[$preserve['action_var']].'_popup'])
1140
		{
1141
			$validated += $content[$value[$preserve['action_var']].'_popup'];
1142
		}
1143
1144
		// Save current column settings as default, clear, or force (admins only)
1145
		if($GLOBALS['egw_info']['user']['apps']['admin'] && $app && $value['selectcols'])
1146
		{
1147
			$pref_name = 'nextmatch-' . (isset($content_value['columnselection_pref']) ? $content_value['columnselection_pref'] : $this->attrs['template']);
1148
			$refresh_pref_name = $pref_name.'-autorefresh';
1149
			switch($value['nm_col_preference']) {
1150
				case 'force':
1151
					$pref_level = 'forced';
1152
					break;
1153
				case 'reset':
1154
				case 'default':
1155
					$pref_level = 'default';
1156
					break;
1157
				default:
1158
					$pref_level = 'user';
1159
			}
1160
1161
			// Clear forced pref before setting default
1162
			if($pref_level != 'forced')
1163
			{
1164
				$GLOBALS['egw']->preferences->delete($app,$pref_name,'forced');
1165
				$GLOBALS['egw']->preferences->delete($app,$refresh_pref_name,'forced');
1166
				$GLOBALS['egw']->preferences->delete($app,$pref_name.'-size','forced');
1167
				$GLOBALS['egw']->preferences->delete($app,$pref_name.'-lettersearch','forced');
1168
				$GLOBALS['egw']->preferences->save_repository(true,'forced');
1169
			}
1170
1171
			// Set columns + refresh as default for all users
1172
			// Columns included in submit, preference might not be updated yet
1173
			$cols = $value['selectcols'];
1174
			$GLOBALS['egw']->preferences->read_repository(true);
1175
			$GLOBALS['egw']->preferences->add($app,$pref_name,is_array($cols) ? implode(',',$cols) : $cols, $pref_level);
1176
1177
			// Autorefresh
1178
			$refresh = $value['nm_autorefresh'];
1179
			$GLOBALS['egw']->preferences->add($app,$refresh_pref_name,(int)$refresh,$pref_level);
1180
1181
			// Lettersearch
1182
			$lettersearch = is_array($cols) && in_array('lettersearch', $cols);
1183
			$GLOBALS['egw']->preferences->add($app,$pref_name.'-lettersearch',(int)$lettersearch,$pref_level);
1184
1185
			$GLOBALS['egw']->preferences->save_repository(true,$pref_level);
1186
			$GLOBALS['egw']->preferences->read(true);
1187
1188
			if($value['nm_col_preference'] == 'reset')
1189
			{
1190
				// Clear column + refresh preference so users go back to default
1191
				$GLOBALS['egw']->preferences->delete_preference($app,$pref_name);
1192
				$GLOBALS['egw']->preferences->delete_preference($app,$pref_name.'-size');
1193
				$GLOBALS['egw']->preferences->delete_preference($app,$pref_name.'-lettersearch');
1194
				$GLOBALS['egw']->preferences->delete_preference($app,$refresh_pref_name);
1195
			}
1196
		}
1197
		unset($value['nm_col_preference']);
1198
1199
		$validated[$form_name] = $value;
1200
	}
1201
1202
	/**
1203
	 * Run a given method on all children
1204
	 *
1205
	 * Reimplemented to add namespace, and make sure row template gets included
1206
	 *
1207
	 * @param string|callable $method_name or function($cname, $expand, $widget)
1208
	 * @param array $params =array('') parameter(s) first parameter has to be cname, second $expand!
1209
	 * @param boolean $respect_disabled =false false (default): ignore disabled, true: method is NOT run for disabled widgets AND their children
1210
	 */
1211
	public function run($method_name, $params=array(''), $respect_disabled=false)
1212
	{
1213
		$old_param0 = $params[0];
1214
		$cname =& $params[0];
1215
		// Need this check or the headers will get involved too
1216
		if($this->type == 'nextmatch')
1217
		{
1218
			parent::run($method_name, $params, $respect_disabled);
1219
			if ($this->id) $cname = self::form_name($cname, $this->id, $params[1]);
1220
1221
			// Run on all the sub-templates
1222
			foreach(array('template', 'header_left', 'header_right', 'header_row') as $sub_template)
1223
			{
1224
				if($this->attrs[$sub_template])
1225
				{
1226
					$row_template = Template::instance($this->attrs[$sub_template]);
1227
					$row_template->run($method_name, $params, $respect_disabled);
1228
				}
1229
			}
1230
		}
1231
		$params[0] = $old_param0;
1232
1233
		// Prevent troublesome keys from breaking the nextmatch
1234
		// TODO: Figure out where these come from
1235
		foreach(array('$row','${row}', '$', '0','1','2') as $key)
1236
		{
1237
			if(is_array(self::$request->content[$cname])) unset(self::$request->content[$cname][$key]);
1238
			if(is_array(self::$request->preserve[$cname])) unset(self::$request->preserve[$cname][$key]);
1239
		}
1240
	}
1241
1242
	/**
1243
	 * Refresh given rows for specified change
1244
	 *
1245
	 * Change type parameters allows for quicker refresh then complete server side reload:
1246
	 * - edit: send just modified data from given rows
1247
	 * - delete: just send null for given rows to clientside (no backend call neccessary)
1248
	 * - add: requires full reload
1249
	 *
1250
	 * @param array|string $row_ids rows to refresh
1251
	 * @param string $type ='edit' "edit" (default), "delete" or "add"
1252
	 */
1253
	public function refresh($row_ids, $type='edit')
1254
	{
1255
		unset($row_ids, $type);	// not used, but required by function signature
1256
1257
		throw new Api\Exception('Not yet implemented');
1258
	}
1259
}
1260
1261
// Registration needs to go here, otherwise customfields won't be loaded until some other cf shows up
1262
Etemplate\Widget::registerWidget(__NAMESPACE__.'\\Customfields', array('nextmatch-customfields'));
1263