Completed
Push — master ( 7928fb...52fb52 )
by Ralf
36:30 queued 18:40
created

inc/class.addressbook_export_contacts_csv.inc.php (17 issues)

1
<?php
2
/**
3
 * EGroupware - addressbook
4
 *
5
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
6
 * @package addressbook
7
 * @subpackage importexport
8
 * @link http://www.egroupware.org
9
 * @author Cornelius Weiss <[email protected]>
10
 * @copyright Cornelius Weiss <[email protected]>
11
 * @version $Id$
12
 */
13
14
use EGroupware\Api;
15
use EGroupware\Api\Acl;
16
17
/**
18
 * export plugin of addressbook
19
 */
20
class addressbook_export_contacts_csv implements importexport_iface_export_plugin
21
{
22
	/**
23
	 * Constants used for exploding categories & multi-selectboxes into seperate fields
24
	 */
25
	const NO_EXPLODE = False;
26
	const MAIN_CATS = 'main_cats';	// Only the top-level categories get their own field
27
	const EACH_CAT = 'each_cat';	// Every category gets its own field
28
	const EXPLODE = 'explode';	// For [custom] multi-selects, each option gets its own field
29
30
	public function __construct()
31
	{
32
		$this->ui= new addressbook_ui();
0 ignored issues
show
Bug Best Practice introduced by
The property ui does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
33
		$this->get_selects();
34
	}
35
36
	/**
37
	 * Exports records as defined in $_definition
38
	 *
39
	 * @param egw_record $_definition
0 ignored issues
show
The type egw_record was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
40
	 */
41
	public function export( $_stream, importexport_definition $_definition) {
42
43
		$options = $_definition->plugin_options;
0 ignored issues
show
Bug Best Practice introduced by
The property plugin_options does not exist on importexport_definition. Since you implemented __get, consider adding a @property annotation.
Loading history...
44
		$this->export_object = $export_object = new importexport_export_csv($_stream, (array)$options);
0 ignored issues
show
Bug Best Practice introduced by
The property export_object does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
45
46
		$selection = array();
47
48
		// Addressbook defines its own export imits
49
		$limit_exception = Api\Storage\Merge::is_export_limit_excepted();
50
		$export_limit = Api\Storage\Merge::getExportLimit($app='addressbook');
51
		if (!$limit_exception) $export_object->export_limit = $export_limit; // we may not need that after all
0 ignored issues
show
Documentation Bug introduced by
It seems like $export_limit can also be of type false. However, the property $export_limit is declared as type integer. 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...
52
		if($export_limit == 'no' && !$limit_exception) {
53
			return;
54
		}
55
56
		// Need to switch the app to get the same results
57
		$old_app = $GLOBALS['egw_info']['flags']['currentapp'];
58
		$GLOBALS['egw_info']['flags']['currentapp'] = 'addressbook';
59
60
		if ($options['selection'] == 'search') {
61
			// uicontacts selection with checkbox 'use_all'
62
			$query = Api\Cache::getSession('addressbook', 'index');
63
			$query['num_rows'] = -1;	// all
64
			$query['csv_export'] = true;	// so get_rows method _can_ produce different content or not store state in the session
65
			$query['order'] = 'contact_id';
66
			if(!array_key_exists('filter',$query)) $query['filter'] = $GLOBALS['egw_info']['user']['account_id'];
67
			$readonlys = null;
68
			$this->ui->get_rows($query,$selection,$readonlys, true);	// only return the ids
69
		}
70
		elseif ( $options['selection'] == 'all' ) {
71
			if ($GLOBALS['egw_info']['user']['preferences']['addressbook']['hide_accounts'] === '1') {
72
				$col_filter['account_id'] = null;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$col_filter was never initialized. Although not strictly required by PHP, it is generally a good practice to add $col_filter = array(); before regardless.
Loading history...
73
			}
74
			$selection = ExecMethod2('addressbook.addressbook_bo.search', array(), true, '', '','',false,'AND',false,$col_filter);
0 ignored issues
show
Deprecated Code introduced by
The function ExecMethod2() has been deprecated: use autoloadable class-names, instanciate and call method or use static methods ( Ignorable by Annotation )

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

74
			$selection = /** @scrutinizer ignore-deprecated */ ExecMethod2('addressbook.addressbook_bo.search', array(), true, '', '','',false,'AND',false,$col_filter);

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...
75
			//$uicontacts->get_rows($query,$selection,$readonlys,true);
76
		}
77
		elseif ($options['selection'] == 'filter')
78
		{
79
			$filter = $_definition->filter;
0 ignored issues
show
Bug Best Practice introduced by
The property filter does not exist on importexport_definition. Since you implemented __get, consider adding a @property annotation.
Loading history...
80
			$query = array();
81
82
			// Handle ranges
83
			foreach($filter as $field => $value)
84
			{
85
				if($field == 'cat_id')
86
				{
87
					$query['col_filter'][$field] = implode(',',$value);
88
					continue;
89
				}
90
91
				// Birthdays in addressbook are formatted Y-m-d
92
				if($field == 'bday')
93
				{
94
					if($value['from'])
95
					{
96
						$query['col_filter'][] = "contact_bday >= " . $GLOBALS['egw']->db->quote(date('Y-m-d', (int)$value['from']));
97
					}
98
					if($value['to'])
99
					{
100
						$query['col_filter'][] = "contact_bday <= " . $GLOBALS['egw']->db->quote(date('Y-m-d', (int)$value['to']));
101
					}
102
					continue;
103
				}
104
				// Custom fields & listed exceptions are not filtered with contact_ prefix
105
				if(strpos($field, '#') !== 0 && !in_array($field, array('tid','owner')))
106
				{
107
					$field = 'contact_'.$field;
108
				}
109
				$query['col_filter'][$field] = $value;
110
				if(!is_array($value) || (!$value['from'] && !$value['to'])) continue;
111
112
				// Ranges are inclusive, so should be provided that way (from 2 to 10 includes 2 and 10)
113
				if($value['from']) $query['col_filter'][] = "$field >= " . (int)$value['from'];
114
				if($value['to']) $query['col_filter'][] = "$field <= " . (int)$value['to'];
115
				unset($query['col_filter'][$field]);
116
			}
117
			$selection = ExecMethod2('addressbook.addressbook_bo.search', array(), true, '', '','',false,'AND',false,$query['col_filter']);
0 ignored issues
show
Deprecated Code introduced by
The function ExecMethod2() has been deprecated: use autoloadable class-names, instanciate and call method or use static methods ( Ignorable by Annotation )

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

117
			$selection = /** @scrutinizer ignore-deprecated */ ExecMethod2('addressbook.addressbook_bo.search', array(), true, '', '','',false,'AND',false,$query['col_filter']);

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...
118
		}
119
		else
120
		{
121
			$selection = explode(',',$options['selection']);
122
		}
123
		if(!is_array($selection))
124
		{
125
			$selection = array();
126
		}
127
		$GLOBALS['egw_info']['flags']['currentapp'] = $old_app;
128
129
		if(Api\Storage\Merge::hasExportLimit($export_limit) && !$limit_exception) {
130
			$selection = array_slice($selection, 0, $export_limit);
131
		}
132
133
		if($options['explode_multiselects']) {
134
			$customfields = Api\Storage\Customfields::get('addressbook');
135
			$additional_fields = array();
136
			$cat_obj = new Api\Categories('', 'addressbook');
137
			foreach($options['explode_multiselects'] as $field => $explode) {
138
				switch($explode['explode']) {
139
					case self::MAIN_CATS:
140
						$cats = $cat_obj->return_array('mains', 0, false,'','ASC','',true);
141
						foreach($cats as $settings) {
142
							$additional_fields[$field][$settings['id']] = array(
143
								'count' => 0,
144
								'label' => $settings['name'],
145
								'subs' => array(),
146
							);
147
							$subs = $cat_obj->return_sorted_array(0, False, '', 'ASC', 'cat_name', True, $settings['id']);
148
							foreach($subs as $sub) {
149
								$name = $sub['name'];
150
								$path = $sub;
151
								while($path['parent'] != $settings['id']) {
152
									$path = $cat_obj->read($path['parent']);
153
									$name = $path['name'] . '/' . $name;
154
								}
155
								$additional_fields[$field][$settings['id']]['subs'][$sub['id']] = $name;
156
							}
157
						}
158
						break;
159
					case self::EACH_CAT:
160
						$cats = $cat_obj->return_array('all', 0, false,'','ASC','',true);
161
						foreach($cats as $settings) {
162
							$name = $settings['name'];
163
							$path = $settings;
164
							while($path['level'] != 0) {
165
								$path = $cat_obj->read($path['parent']);
166
								$name = $path['name'] . '/' . $name;
167
							}
168
							$additional_fields[$field][$settings['id']] = array(
169
								'count' => 0,
170
								'label' => $name
171
							);
172
						}
173
						break;
174
					case self::EXPLODE:
175
						// Only works for custom fields
176
						$index = substr($field, 1);
177
						foreach($customfields[$index]['values'] as $key => $value) {
178
							$additional_fields[$field][$key] = array(
179
								'count' => 0,
180
								'label' => $customfields[$index]['label'] . ': ' . $value,
181
							);
182
						}
183
						break;
184
				}
185
			}
186
187
			// Check records to see if additional fields are actually used
188
			foreach ($selection as $_contact) {
189
				if(is_array($_contact) && array_key_exists('photo', $_contact)) {
190
					unset($_contact['photo']);
191
				}
192
				if(is_array($_contact) && count($_contact) == 1 && $_contact['id']) {
193
					$_contact = $_contact['id'];
194
				}
195
				if(is_array($_contact) && $_contact['id']) {
196
					$contact = new addressbook_egw_record();
197
					$contact->set_record($_contact);
198
				} else {
199
					$contact = new addressbook_egw_record($_contact);
200
				}
201
				foreach($additional_fields as $field => &$values) {
202
					if(!$contact->$field) continue;
203
					foreach($values as $value => &$settings) {
204
						if(!is_array($contact->$field)) {
205
							$contact->$field = explode(',', $contact->$field);
206
						}
207
						if(is_array($contact->$field) && in_array($value, $contact->$field)) {
208
							$settings['count']++;
209
						} elseif($contact->$field == $value) {
210
							$settings['count']++;
211
						} elseif($options['explode_multiselects'][$field]['explode'] == self::MAIN_CATS && array_intersect($contact->$field, array_keys($settings['subs']))) {
212
							$settings['count']++;
213
						}
214
					}
215
				}
216
			}
217
218
			unset($field);
219
			unset($value);
220
			unset($settings);
221
222
			// Add additional columns
223
			foreach($additional_fields as $field => $additional_values) {
224
				// Remove original
225
				unset($options['mapping'][$field]);
226
				// Add exploded
227
				$field_count = 0;
228
				foreach($additional_values as $value => $settings) {
229
					if($settings['count'] > 0) {
230
						$field_count += $settings['count'];
231
						$options['mapping'][$field.'-'.$value] = $settings['label'];
232
					}
233
				}
234
				if($field_count > 0) {
235
					// Set some options for converting
236
					$options['explode_multiselects'][$field]['values'] = $additional_values;
237
				} else {
238
					// Don't need this anymore
239
					unset($options['explode_multiselects'][$field]);
240
				}
241
			}
242
		}
243
244
		$export_object->set_mapping($options['mapping']);
245
246
		// Add in last/next event, if needed
247
		if($options['mapping']['last_date'] || $options['mapping']['next_date'])
248
		{
249
			$contact_ids = array();
250
			foreach($selection as $_contact)
251
			{
252
				if(is_array($_contact) && $_contact['id'])
253
				{
254
					$contact_ids[] = $_contact['account_id'] ? $_contact['account_id'] : 'c'.$_contact['id'];
255
				}
256
				else
257
				{
258
					$contact_ids[] = 'c'.$contact;
259
				}
260
			}
261
			$events = $this->ui->read_calendar($contact_ids, false);
262
		}
263
264
		// $options['selection'] is array of identifiers as this plugin doesn't
265
		// support other selectors atm.
266
		foreach ($selection as $_contact) {
267
			if(is_array($_contact) && array_key_exists('photo', $_contact)) {
268
				unset($_contact['photo']);
269
			}
270
			if(is_array($_contact) && count($_contact) == 1 && $_contact['id']) {
271
				$_contact = $_contact['id'];
272
			}
273
			if(is_array($_contact) && $_contact['id']) {
274
				$contact = new addressbook_egw_record();
275
				$contact->set_record($_contact);
276
			} else {
277
				$contact = new addressbook_egw_record($_contact);
278
			}
279
280
			if($events && $events[$contact->id])
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on addressbook_egw_record. Since you implemented __get, consider adding a @property annotation.
Loading history...
281
			{
282
				// NB: last_date and next_date are used instead of last_event & next_event
283
				// to avoid automatic conversion - we want to export link title, not date-time
284
				$contact->last_date = $events[$contact->id]['last_link']['title'];
0 ignored issues
show
Bug Best Practice introduced by
The property last_date does not exist on addressbook_egw_record. Since you implemented __set, consider adding a @property annotation.
Loading history...
285
				$contact->next_date = $events[$contact->id]['next_link']['title'];
0 ignored issues
show
Bug Best Practice introduced by
The property next_date does not exist on addressbook_egw_record. Since you implemented __set, consider adding a @property annotation.
Loading history...
286
			}
287
			// Some conversion
288
			$this->convert($contact, $options);
289
			if($options['convert']) {
290
				importexport_export_csv::convert($contact, addressbook_egw_record::$types, 'addressbook',$this->selects);
291
			} else {
292
				// Implode arrays, so they don't say 'Array'
293
				foreach($contact->get_record_array() as $key => $value) {
294
					if(is_array($value)) $contact->$key = implode(',', $value);
295
				}
296
			}
297
298
			$export_object->export_record($contact);
299
			unset($contact);
300
		}
301
		return $export_object;
302
	}
303
304
	/**
305
	 * returns translated name of plugin
306
	 *
307
	 * @return string name
308
	 */
309
	public static function get_name() {
310
		return lang('Addressbook CSV export');
311
	}
312
313
	/**
314
	 * returns translated (user) description of plugin
315
	 *
316
	 * @return string descriprion
317
	 */
318
	public static function get_description() {
319
		return lang("Exports contacts from your Addressbook into a CSV File.");
320
	}
321
322
	/**
323
	 * retruns file suffix for exported file
324
	 *
325
	 * @return string suffix
326
	 */
327
	public static function get_filesuffix() {
328
		return 'csv';
329
	}
330
331
	public static function get_mimetype() {
332
		return 'text/csv';
333
	}
334
335
	/**
336
	 * Suggest a file name for the downloaded file
337
	 * No suffix
338
	 */
339
	public function get_filename()
340
	{
341
		if(is_object($this->export_object) && $this->export_object->get_num_of_records() == 1)
342
		{
343
			return $this->export_object->record->get_title();
344
		}
345
		return false;
346
	}
347
348
	/**
349
	 * return html for options.
350
	 * this way the plugin has all opertunities for options tab
351
	 *
352
	 * @param $definition Specific definition
0 ignored issues
show
The type Specific was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
353
	 *
354
	 * @return array (
355
	 * 		name 		=> string,
356
	 * 		content		=> array,
357
	 * 		sel_options	=> array,
358
	 * 		readonlys	=> array,
359
	 * 		preserv		=> array,
360
	 * )
361
	 */
362
	public function get_options_etpl(importexport_definition &$definition = NULL)
363
	{
364
		return false;
365
	}
366
367
	/**
368
	 * returns slectors of this plugin via xajax
369
	 *
370
	 */
371
	public function get_selectors_etpl() {
372
		return array(
373
			'name'		=> 'importexport.export_csv_selectors',
374
		);
375
	}
376
377
	/**
378
	* Convert some internal data to something with more meaning
379
	*
380
	* Dates, times, user IDs, category IDs
381
	*/
382
	public static function convert(addressbook_egw_record &$record, $options) {
383
384
		if ($record->tel_prefer) {
0 ignored issues
show
Bug Best Practice introduced by
The property tel_prefer does not exist on addressbook_egw_record. Since you implemented __get, consider adding a @property annotation.
Loading history...
385
			$field = $record->tel_prefer;
386
			$record->tel_prefer = $record->$field;
0 ignored issues
show
Bug Best Practice introduced by
The property tel_prefer does not exist on addressbook_egw_record. Since you implemented __set, consider adding a @property annotation.
Loading history...
387
		}
388
389
		if(!is_array($options['explode_multiselects']))
390
		{
391
			return;
392
		}
393
		foreach((array)$options['explode_multiselects'] as $field => $explode_settings) {
394
			if(!is_array($record->$field)) $record->$field = explode(',', $record->$field);
395
			foreach((array)$explode_settings['values'] as $value => $settings) {
396
				$field_name = "$field-$value";
397
				$record->$field_name = array();
398
				if(is_array($record->$field) && in_array($value, $record->$field) || $record->$field == $value) {
0 ignored issues
show
Consider adding parentheses for clarity. Current Interpretation: (is_array($record->$fiel...ecord->$field == $value, Probably Intended Meaning: is_array($record->$field...cord->$field == $value)
Loading history...
399
					if($explode_settings['explode'] != self::MAIN_CATS) {
400
						$record->$field_name = $options['convert'] ? lang('Yes') : true;
401
					} elseif($options['convert']) {
402
						// 3 part assign due to magic get method
403
						$record_value = $record->$field_name;
404
						$record_value[] = $settings['label'];
405
						$record->$field_name = $record_value;
406
					} else {
407
						$record->$field_name = $value;
408
					}
409
				}
410
				if($explode_settings['explode'] == self::MAIN_CATS && count(array_intersect($record->$field, array_keys($settings['subs'])))) {
411
					// 3 part assign due to magic get method
412
					$record_value = $record->$field_name;
413
					if(!is_array($record_value)) $record_value = array($record_value);
414
					foreach(array_intersect($record->$field, array_keys($settings['subs'])) as $sub_id) {
415
						$record_value[] = $options['convert'] ? $settings['subs'][$sub_id] : $sub_id;
416
					}
417
					$record->$field_name = $record_value;
418
				}
419
				if(is_array($record->$field_name)) $record->$field_name = implode(($options['convert'] ? ', ' : ','), $record->$field_name);
420
			}
421
		}
422
	}
423
424
425
	protected function get_selects()
426
	{
427
		$this->selects = array(
0 ignored issues
show
Bug Best Practice introduced by
The property selects does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
428
			'tid' => array('n' => 'Contact')
429
		);
430
		foreach($this->ui->content_types as $tid => $data)
431
		{
432
			$this->selects['tid'][$tid] = $data['name'];
433
		}
434
	}
435
	/**
436
	 * Get the class name for the egw_record to use while exporting
437
	 *
438
	 * @return string;
439
	 */
440
	public static function get_egw_record_class()
441
	{
442
		return 'addressbook_egw_record';
443
	}
444
445
	/**
446
	 * Adjust automatically generated filter fields
447
	 */
448
	public function get_filter_fields(Array &$filters)
449
    {
450
		unset($filters['last_event']);
451
		unset($filters['next_event']);
452
		foreach($filters as $field_name => &$settings)
453
		{
454
			if($this->selects[$field_name]) $settings['values'] = $this->selects[$field_name];
455
		}
456
		$filters['owner'] = array(
457
			'name'		=> 'owner',
458
			'label'		=> 'addressbook',
459
			'type'		=> 'select',
460
			'rows'		=> 5,
461
			'tags'		=> true,
462
			'values'	=> $this->ui->get_addressbooks(Acl::READ)
463
		);
464
	}
465
}
466