Completed
Push — 16.1 ( 1e4888...9df24e )
by Ralf
12:06
created

Customfields::handle_file()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 6
nc 3
nop 3
dl 0
loc 12
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * EGroupware - eTemplate custom fields 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 Nathan Gray
10
 * @copyright 2011 Nathan Gray
11
 * @version $Id$
12
 */
13
14
namespace EGroupware\Api\Etemplate\Widget;
15
16
use EGroupware\Api;
17
18
/**
19
 * Widgets for custom fields and listing custom fields
20
 */
21
class Customfields extends Transformer
22
{
23
24
	/**
25
	 * Allowd types of customfields
26
	 *
27
	 * The additionally allowed app-names from the link-class, will be add by the edit-method only,
28
	 * as the link-class has to be called, which can NOT be instanciated by the constructor, as
29
	 * we get a loop in the instanciation.
30
	 *
31
	 * @var array
32
	 */
33
	protected static $cf_types = array(
34
		'text'     => 'Text',
35
		'int'      => 'Integer',
36
		'float'    => 'Float',
37
		'label'    => 'Label',
38
		'select'   => 'Selectbox',
39
		'ajax_select' => 'Search',
40
		'radio'    => 'Radiobutton',
41
		'checkbox' => 'Checkbox',
42
		'date'     => 'Date',
43
		'date-time'=> 'Date+Time',
44
		'select-account' => 'Select account',
45
		'button'   => 'Button',         // button to execute javascript
46
		'url'      => 'Url',
47
		'url-email'=> 'EMail',
48
		'url-phone'=> 'Phone number',
49
		'htmlarea' => 'Formatted Text (HTML)',
50
		'link-entry' => 'Select entry',         // should be last type, as the individual apps get added behind
51
	);
52
53
	/**
54
	 * @var $prefix string Prefix for every custiomfield name returned in $content (# for general (admin) customfields)
55
	 */
56
	protected static $prefix = '#';
57
58
	// Make settings available globally
59
	const GLOBAL_VALS = '~custom_fields~';
60
61
	// Used if there's no ID provided
62
	const GLOBAL_ID = 'custom_fields';
63
64
	protected $legacy_options = 'sub-type,use-private,field-names';
65
66
	protected static $transformation = array(
67
		'type' => array(
68
			'customfields-types' => array(
69
				'type'	=>	'select',
70
				'sel_options'	=> array()
71
			),
72
			'customfields-list' => array(
73
				'readonly'	=> true
74
			)
75
		)
76
	);
77
78
	public function __construct($xml)
79
	{
80
		parent::__construct($xml);
81
	}
82
83
	/**
84
	 * Fill type options in self::$request->sel_options to be used on the client
85
	 *
86
	 * @param string $cname
87
	 * @param array $expand values for keys 'c', 'row', 'c_', 'row_', 'cont'
88
	 */
89
	public function beforeSendToClient($cname, array $expand=null)
90
	{
91
		// No name, no way to get parameters client-side.
92
		if(!$this->id) $this->id = self::GLOBAL_ID;
93
94
		$form_name = self::form_name($cname, $this->id, $expand);
95
96
		// Store properties at top level, so all customfield widgets can share
97
		if($this->attrs['app'])
98
		{
99
			$app = $this->attrs['app'];
100
		}
101 View Code Duplication
		else
102
		{
103
			$app =& $this->getElementAttribute(self::GLOBAL_VALS, 'app');
104
			if($this->getElementAttribute($form_name, 'app'))
105
			{
106
				$app =& $this->getElementAttribute($form_name, 'app');
107
			}
108
			else
109
			{
110
				// Checking creates it even if it wasn't there
111
				unset(self::$request->modifications[$form_name]['app']);
112
			}
113
		}
114
115 View Code Duplication
		if($this->getElementAttribute($form_name, 'customfields'))
116
		{
117
			$customfields =& $this->getElementAttribute($form_name, 'customfields');
118
		}
119
		elseif($app)
120
		{
121
			// Checking creates it even if it wasn't there
122
			unset(self::$request->modifications[$form_name]['customfields']);
123
			$customfields =& $this->getElementAttribute(self::GLOBAL_VALS, 'customfields');
124
		}
125
126
		if(!$app)
127
		{
128
			$app =& $this->setElementAttribute(self::GLOBAL_VALS, 'app', $GLOBALS['egw_info']['flags']['currentapp']);
129
			$customfields =& $this->setElementAttribute(self::GLOBAL_VALS, 'customfields', Api\Storage\Customfields::get($app));
130
		}
131
132
		// if we are in the etemplate editor or the app has no cf's, load the cf's from the app the tpl belongs too
133
		if ($app && $app != 'stylite' && $app != $GLOBALS['egw_info']['flags']['currentapp'] && !isset($customfields) &&
134
			($GLOBALS['egw_info']['flags']['currentapp'] == 'etemplate' || !$this->attrs['customfields']) || !isset($customfields))
135
		{
136
			// app changed
137
			$customfields =& Api\Storage\Customfields::get($app);
138
		}
139
		// Filter fields
140
		if($this->attrs['field-names'])
141
		{
142
			$fields_name = explode(',', $this->attrs['field-names']);
143
			foreach($fields_name as &$f)
144
			{
145
				if ($f[0] == "!")
146
				{
147
					$f= substr($f,1);
148
					$negate_fields[]= $f;
149
				}
150
				$field_filters []= $f;
151
			}
152
		}
153
154
		$fields = $customfields;
155
156
		$use_private = self::expand_name($this->attrs['use-private'],0,0,'','',self::$cont);
157
		$this->attrs['sub-type'] = self::expand_name($this->attrs['sub-type'],0,0,'','',self::$cont);
158
159
		foreach((array)$fields as $key => $field)
160
		{
161
			// remove private or non-private cf's, if only one kind should be displayed
162
			if ((string)$use_private !== '' && (boolean)$field['private'] != (boolean)$use_private)
163
			{
164
				unset($fields[$key]);
165
			}
166
167
			// Remove filtered fields
168
			if($field_filters && in_array($key, $negate_fields) && in_array($key, $field_filters))
169
			{
170
				unset($fields[$key]);
171
			}
172
		}
173
		// check if name refers to a single custom field --> show only that
174
		$matches = null;
175
		if (($pos=strpos($form_name,self::$prefix)) !== false && // allow the prefixed name to be an array index too
176
			preg_match($preg = '/'.self::$prefix.'([^\]]+)/',$form_name,$matches) && isset($fields[$name=$matches[1]]))
177
		{
178
			$fields = array($name => $fields[$name]);
179
			$value = array(self::$prefix.$name => $value);
0 ignored issues
show
Bug introduced by
The variable $value seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
180
			$form_name = self::$prefix.$name;
181
		}
182
183
		if(!is_array($fields)) $fields = array();
184
		switch($type = $this->type)
185
		{
186
			case 'customfields-types':
187
				foreach(self::$cf_types as $lname => $label)
188
				{
189
					$sel_options[$lname] = lang($label);
190
					$fields_with_vals[]=$lname;
191
				}
192
				$link_types = array_intersect_key(Api\Link::app_list('query'), Api\Link::app_list('title'));
193
				// Explicitly add in filemanager, which does not support query or title
194
				$link_types['filemanager'] = lang('filemanager');
195
196
				ksort($link_types);
197
				foreach($link_types as $lname => $label)
198
				{
199
					$sel_options[$lname] = '- '.$label;
200
				}
201
				self::$transformation['type'][$type]['sel_options'] = $sel_options;
202
				self::$transformation['type'][$type]['no_lang'] = true;
203
				return parent::beforeSendToClient($cname, $expand);
0 ignored issues
show
Bug introduced by
It seems like $expand defined by parameter $expand on line 89 can also be of type null; however, EGroupware\Api\Etemplate...r::beforeSendToClient() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
204
			case 'customfields-list':
205
				foreach(array_reverse($fields) as $lname => $field)
206
				{
207
					if (!empty($this->attrs['sub-type']) && !empty($field['type2']) &&
208
						strpos(','.$field['type2'].',',','.$field['type2'].',') === false) continue;    // not for our content type//
209
					if (isset($value[self::$prefix.$lname]) && $value[self::$prefix.$lname] !== '') //break;
210
					{
211
						$fields_with_vals[]=$lname;
212
					}
213
					//$stop_at_field = $name;
214
				}
215
				break;
216
			default:
217
				foreach(array_reverse($fields) as $lname => $field)
218
				{
219
					$fields_with_vals[]=$lname;
220
				}
221
		}
222
		// need to encode values/select-options to keep their order
223
		foreach($customfields as &$data)
224
		{
225
			if (!empty($data['values']))
226
			{
227
				Select::fix_encoded_options($data['values']);
228
			}
229
		}
230
		if($fields != $customfields)
231
		{
232
			// This widget has different settings from global
233
			$this->setElementAttribute($form_name, 'customfields', $fields);
234
			$this->setElementAttribute($form_name, 'fields', array_merge(
235
				array_fill_keys(array_keys($customfields), false),
236
				array_fill_keys(array_keys($fields), true)
237
			));
238
		}
239
		parent::beforeSendToClient($cname, $expand);
0 ignored issues
show
Bug introduced by
It seems like $expand defined by parameter $expand on line 89 can also be of type null; however, EGroupware\Api\Etemplate...r::beforeSendToClient() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
240
241
		// Re-format date custom fields from Y-m-d
242
		$field_settings =& self::get_array(self::$request->modifications, "{$this->id}[customfields]",true);
243
		if (true) $field_settings = array();
244
		$link_types = Api\Link::app_list();
245
		foreach($fields as $fname => $field)
246
		{
247
			// Run beforeSendToClient for each field
248
			$widget = $this->_widget($fname, $field);
249
			if(method_exists($widget, 'beforeSendToClient'))
250
			{
251
				$widget->beforeSendToClient($this->id == self::GLOBAL_ID ? '' : $this->id, $expand);
252
			}
253
		}
254
	}
255
256
	/**
257
	 * Instanciate (server-side) widget used to implement custom-field, to run its beforeSendToClient or validate method
258
	 *
259
	 * @param string $fname custom field name
260
	 * @param array $field custom field data
261
	 * @return Etemplate\Widget
262
	 */
263
	protected function _widget($fname, array $field)
264
	{
265
		static $link_types = null;
266
		if (!isset($link_types)) $link_types = Api\Link::app_list ();
267
268
		$type = $field['type'];
269
		// Link-tos needs to change from appname to link-to
270
		if($link_types[$field['type']])
271
		{
272
			if($type == 'filemanager')
273
			{
274
				$type = 'vfs-upload';
275
			}
276
			else
277
			{
278
				$type = 'link-to';
279
			}
280
		}
281
		$xml = '<'.$type.' type="'.$type.'" id="'.self::$prefix.$fname.'"/>';
282
		$widget = self::factory($type, $xml, self::$prefix.$fname);
283
		$widget->id = self::$prefix.$fname;
284
		$widget->attrs['type'] = $type;
285
		$widget->set_attrs($xml);
286
		
287
		// some type-specific (default) attributes
288
		switch($type)
289
		{
290
			case 'date':
291
			case 'date-time':
292
				$widget->attrs['dataformat'] = $type == 'date' ? 'Y-m-d' : 'Y-m-d H:i:s';
293 View Code Duplication
				if($field['values']['min']) $widget->attrs['min'] = $field['values']['min'];
294 View Code Duplication
				if($field['values']['max']) $widget->attrs['min'] = $field['values']['max'];
295
				break;
296
297
			case 'vfs-upload':
298
				$widget->attrs['path'] = $field['app'] . ':' .
299
					self::expand_name('$cont['.Api\Link::get_registry($field['app'],'view_id').']',0,0,0,0,self::$request->content).
300
					':'.$field['label'];
301
				break;
302
303
			case 'link-to':
304
				$widget->attrs['only_app'] = $field['type'];
305
				break;
306
307
			case 'text':
308
				break;
309
310
			default:
311
				if (substr($type, 0, 7) !== 'select-' && $type != 'ajax_select') break;
312
				// fall-through for all select-* widgets
313
			case 'select':
0 ignored issues
show
Unused Code introduced by
// fall-through for all ...] = $field['rows'] > 1; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
314
				$this->attrs['multiple'] = $field['rows'] > 1;
315
				// fall through
316
			case 'radio':
317 View Code Duplication
				if (count($field['values']) == 1 && isset($field['values']['@']))
318
				{
319
					$field['values'] = Api\Storage\Customfields::get_options_from_file($field['values']['@']);
320
				}
321
				// keep extra values set by app code, eg. addressbook advanced search
322
				if (is_array(self::$request->sel_options[self::$prefix.$fname]))
323
				{
324
					self::$request->sel_options[self::$prefix.$fname] += (array)$field['values'];
325
				}
326
				else
327
				{
328
					self::$request->sel_options[self::$prefix.$fname] = $field['values'];
329
				}
330
				//error_log(__METHOD__."('$fname', ".array2string($field).") request->sel_options['".self::$prefix.$fname."']=".array2string(self::$request->sel_options[$this->id]));
331
				// to keep order of numeric values, we have to explicit run fix_encoded_options, as sel_options are already encoded
332
				$options = self::$request->sel_options[self::$prefix.$fname];
333
				if (is_array($options))
334
				{
335
					Select::fix_encoded_options($options);
336
					self::$request->sel_options[self::$prefix.$fname] = $options;
337
				}
338
				break;
339
		}
340
		return $widget;
341
	}
342
343
	/**
344
	 * Validate input
345
	 *
346
	 * Following attributes get checked:
347
	 * - needed: value must NOT be empty
348
	 * - min, max: int and float widget only
349
	 * - maxlength: maximum length of string (longer strings get truncated to allowed size)
350
	 * - preg: perl regular expression incl. delimiters (set by default for int, float and colorpicker)
351
	 * - int and float get casted to their type
352
	 *
353
	 * @param string $cname current namespace
354
	 * @param array $expand values for keys 'c', 'row', 'c_', 'row_', 'cont'
355
	 * @param array $content
356
	 * @param array &$validated=array() validated content
357
	 */
358
	public function validate($cname, array $expand, array $content, &$validated=array())
359
	{
360
		if ($this->id)
361
		{
362
			$form_name = self::form_name($cname, $this->id, $expand);
363
		}
364
		else
365
		{
366
			$form_name = self::GLOBAL_ID;
367
		}
368
369
		$all_readonly = $this->is_readonly($cname, $form_name);
370
		$value_in = self::get_array($content, $form_name);
371
		// if we have no id / use self::GLOBAL_ID, we have to set $value_in in global namespace for regular widgets validation to find
372
		if (!$this->id) $content = array_merge($content, $value_in);
373
		//error_log(__METHOD__."($cname, ...) form_name=$form_name, use-private={$this->attrs['use-private']}, value_in=".array2string($value_in));
374
		$customfields =& $this->getElementAttribute(self::GLOBAL_VALS, 'customfields');
375
		if(is_array($value_in))
376
		{
377
			foreach($value_in as $field => $value)
378
			{
379
				$field_settings = $customfields[$fname=substr($field,1)];
380
381
				if ((string)$this->attrs['use-private'] !== '' &&	// are only (non-)private fields requested
382
					(boolean)$field_settings['private'] != ($this->attrs['use-private'] != '0'))
383
				{
384
					continue;
385
				}
386
387
				// check if single field is set readonly, used in apps as it was only way to make cfs readonly in old eT
388
				// single fields set to false in $readonly overwrite a global __ALL__
389
				$cf_readonly = $this->is_readonly($form_name != self::GLOBAL_ID ? $form_name : $cname, $field);
390
				if ($cf_readonly || $all_readonly && $cf_readonly !== false)
391
				{
392
					continue;
393
				}
394
				// run validation method of widget implementing this custom field
395
				$widget = $this->_widget($fname, $field_settings);
396
				// widget has no validate method, eg. is only displaying stuff --> nothing to validate
397
				if (!method_exists($widget, 'validate')) continue;
398
				$widget->validate($form_name != self::GLOBAL_ID ? $form_name : $cname, $expand, $content, $validated);
399
				$field_name = $this->id[0] == self::$prefix && $customfields[substr($this->id,1)] ? $this->id : self::form_name($form_name != self::GLOBAL_ID ? $form_name : $cname, $field);
400
				$valid =& self::get_array($validated, $field_name, true);
401
402
				if (is_array($valid)) $valid = implode(',', $valid);
403
				// NULL is valid for most fields, but not custom fields due to backend handling
404
				// See so_sql_cf->save()
405
				if (is_null($valid)) $valid = false;
406
				//error_log(__METHOD__."() $field_name: ".array2string($value).' --> '.array2string($valid));
407
			}
408
		}
409
		elseif ($this->type == 'customfields-types')
410
		{
411
			// Transformation doesn't handle validation
412
			$valid =& self::get_array($validated, $this->id ? $form_name : $field, true);
0 ignored issues
show
Bug introduced by
The variable $field seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
413
			if (true) $valid = $value_in;
414
			//error_log(__METHOD__."() $form_name $field: ".array2string($value).' --> '.array2string($value));
415
		}
416
	}
417
}
418