Issues (4868)

inc/class.addressbook_import_contacts_csv.inc.php (23 issues)

1
<?php
2
/**
3
 * EGroupware Addressbook
4
 *
5
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
6
 * @package importexport
7
 * @link http://www.egroupware.org
8
 * @author Cornelius Weiss <[email protected]>
9
 * @copyright Cornelius Weiss <[email protected]>
10
 * @version $Id$
11
 */
12
13
use EGroupware\Api;
14
15
/**
16
 * class import_csv for addressbook
17
 */
18
class addressbook_import_contacts_csv extends importexport_basic_import_csv  {
19
20
	/**
21
	 * conditions for actions
22
	 *
23
	 * @var array
24
	 */
25
	protected static $conditions = array( 'exists', 'equal' );
26
27
	/**
28
	 * @var addressbook_bo
29
	 */
30
	private $bocontacts;
31
32
	/**
33
	 * For figuring out if a contact has changed
34
	 *
35
	 * @var Api\Contacts\Tracking
36
	 */
37
	protected $tracking;
38
39
	/**
40
	 * @var boolean If import file has no type, it can generate a lot of warnings.
41
	 * Users don't like this, so we only warn once.
42
	 */
43
	private $type_warned = false;
44
45
	/**
46
	 * To empty addressbook before importing, we actually keep track of
47
	 * what's imported and delete the others to keep history.
48
	 *
49
	 * @var type
50
	 */
51
	private $ids = array();
52
53
	/**
54
	 * imports entries according to given definition object.
55
	 * @param resource $_stream
56
	 * @param string $_charset
57
	 * @param definition $_definition
0 ignored issues
show
The type definition 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...
58
	 */
59
	public function import( $_stream, importexport_definition $_definition ) {
60
		parent::import($_stream, $_definition);
61
62
		if($_definition->plugin_options['empty_addressbook'])
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...
63
		{
64
			$this->empty_addressbook($this->user, $this->ids);
65
		}
66
	}
67
68
	/**
69
	 * imports entries according to given definition object.
70
	 * @param resource $_stream
71
	 * @param string $_charset
72
	 * @param definition $_definition
73
	 */
74
	public function init(importexport_definition &$_definition ) {
75
76
		// fetch the addressbook bo
77
		$this->bocontacts = new addressbook_bo();
78
79
		// Get the tracker for changes
80
		$this->tracking = new Api\Contacts\Tracking($this->bocontacts);
81
82
		$this->lookups = array(
83
			'tid' => array('n'=>'contact')
84
		);
85
		foreach($this->bocontacts->content_types as $tid => $data)
86
		{
87
			$this->lookups['tid'][$tid] = $data['name'];
88
		}
89
90
		// Try and set a default type, for use if file does not specify
91
		if(!$this->lookups['tid'][Api\Contacts\Storage::DELETED_TYPE] && count($this->lookups['tid']) == 1 ||
0 ignored issues
show
Consider adding parentheses for clarity. Current Interpretation: (! $this->lookups['tid']...s->lookups['tid']) == 2, Probably Intended Meaning: ! $this->lookups['tid'][...->lookups['tid']) == 2)
Loading history...
92
			$this->lookups['tid'][Api\Contacts\Storage::DELETED_TYPE] && count($this->lookups['tid']) == 2)
93
		{
94
			reset($this->lookups['tid']);
95
			$this->default_type = key($this->lookups['tid']);
0 ignored issues
show
Bug Best Practice introduced by
The property default_type does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
96
		}
97
98
99
		// set contact owner
100
		$contact_owner = isset( $_definition->plugin_options['contact_owner'] ) ?
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...
101
			$_definition->plugin_options['contact_owner'] : $this->user;
102
103
		// Check to make sure target addressbook is valid
104
		if(!in_array($contact_owner, array_keys($this->bocontacts->get_addressbooks(Api\Acl::ADD))))
105
		{
106
			$this->warnings[0] = lang("Unable to import into %1, using %2",
0 ignored issues
show
Bug Best Practice introduced by
The property warnings does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
107
				$contact_owner . ' ('.Api\Accounts::username($record->owner) . ')',
0 ignored issues
show
The call to lang() has too many arguments starting with $contact_owner . ' (' . ...e($record->owner) . ')'. ( Ignorable by Annotation )

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

107
			$this->warnings[0] = /** @scrutinizer ignore-call */ lang("Unable to import into %1, using %2",

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
Comprehensibility Best Practice introduced by
The variable $record seems to be never defined.
Loading history...
108
				Api\Accounts::username($this->user)
109
			);
110
			$contact_owner = 'personal';
111
		}
112
113
		// Import into importer's personal addressbook
114
		if($contact_owner == 'personal')
115
		{
116
			$contact_owner = $this->user;
117
		}
118
		$this->user = $contact_owner;
119
	}
120
121
	/**
122
	 * Import a single record
123
	 *
124
	 * You don't need to worry about mappings or translations, they've been done already.
125
	 * You do need to handle the conditions and the actions taken.
126
	 *
127
	 * Updates the count of actions taken
128
	 *
129
	 * @return boolean success
130
	 */
131
	protected function import_record(importexport_iface_egw_record &$record, &$import_csv)
132
	{
133
		// Set owner, unless it's supposed to come from CSV file
134
		if($this->definition->plugin_options['owner_from_csv'] && $record->owner) {
0 ignored issues
show
Bug Best Practice introduced by
The property owner does not exist on importexport_iface_egw_record. Since you implemented __get, consider adding a @property annotation.
Loading history...
135
			if(!is_numeric($record->owner)) {
136
				// Automatically handle text owner without explicit translation
137
				$new_owner = importexport_helper_functions::account_name2id($record->owner);
138
				if($new_owner == '') {
139
					$this->errors[$import_csv->get_current_position()] = lang(
140
						'Unable to convert "%1" to account ID.  Using plugin setting (%2) for owner.',
141
						$record->owner,
0 ignored issues
show
The call to lang() has too many arguments starting with $record->owner. ( Ignorable by Annotation )

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

141
					$this->errors[$import_csv->get_current_position()] = /** @scrutinizer ignore-call */ lang(

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
142
						Api\Accounts::username($this->user)
143
					);
144
					$record->owner = $this->user;
0 ignored issues
show
Bug Best Practice introduced by
The property owner does not exist on importexport_iface_egw_record. Since you implemented __set, consider adding a @property annotation.
Loading history...
145
				} else {
146
					$record->owner = $new_owner;
147
				}
148
			}
149
		} else {
150
			$record->owner = $this->user;
151
		}
152
153
		// Check that owner (addressbook) is allowed
154
		if(!array_key_exists($record->owner, $this->bocontacts->get_addressbooks()))
155
		{
156
			$this->errors[$import_csv->get_current_position()] = lang("Unable to import into %1, using %2",
157
				Api\Accounts::username($record->owner),
158
				Api\Accounts::username($this->user)
159
			);
160
			$record->owner = $this->user;
161
		}
162
163
		// Do not allow owner == 0 (accounts) without an account_id
164
		// It causes the contact to be filed as an account, and can't delete
165
		if(!$record->owner && !$record->account_id)
0 ignored issues
show
Bug Best Practice introduced by
The property account_id does not exist on importexport_iface_egw_record. Since you implemented __get, consider adding a @property annotation.
Loading history...
166
		{
167
			$record->owner = $GLOBALS['egw_info']['user']['account_id'];
168
		}
169
170
		// Do not import into non-existing type, warn and change
171
		if(!$record->tid || !$this->lookups['tid'][$record->tid])
0 ignored issues
show
Bug Best Practice introduced by
The property tid does not exist on importexport_iface_egw_record. Since you implemented __get, consider adding a @property annotation.
Loading history...
172
		{
173
			// Avoid lots of warnings about type (2 types are contact and deleted)
174
			if($record->tid && !$this->type_warned[$record->tid] && !$this->lookups['tid'][$record->tid] )
175
			{
176
				$this->warnings[$import_csv->get_current_position()] = lang('Unknown type %1, imported as %2',$record->tid,lang($this->lookups['tid']['n']));
0 ignored issues
show
Bug Best Practice introduced by
The property warnings does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
177
				$this->type_warned[$record->tid] = true;
178
			}
179
			$record->tid = $this->default_type;
0 ignored issues
show
Bug Best Practice introduced by
The property tid does not exist on importexport_iface_egw_record. Since you implemented __set, consider adding a @property annotation.
Loading history...
180
		}
181
182
		// Also handle categories in their own field
183
		$record_array = $record->get_record_array();
184
		$more_categories = array();
185
		foreach($this->definition->plugin_options['field_mapping'] as $field_name) {
186
			if(!array_key_exists($field_name, $record_array) ||
187
				substr($field_name,0,3) != 'cat' || !$record->$field_name || $field_name == 'cat_id') continue;
188
			list(, $cat_id) = explode('-', $field_name);
189
			if(is_numeric($record->$field_name) && $record->$field_name != 1) {
190
				// Column has a single category ID
191
				$more_categories[] = $record->$field_name;
192
			} elseif($record->$field_name == '1' ||
193
				(!is_numeric($record->$field_name) && strtolower($record->$field_name) == strtolower(lang('Yes')))) {
194
				// Each category got its own column.  '1' is the database value, lang('yes') is the human value
195
				$more_categories[] = $cat_id;
196
			} else {
197
				// Text categories
198
				$more_categories = array_merge($more_categories, importexport_helper_functions::cat_name2id(is_array($record->$field_name) ? $record->$field_name : explode(',',$record->$field_name), $cat_id));
199
			}
200
		}
201
		if(count($more_categories) > 0) $record->cat_id = array_merge(is_array($record->cat_id) ? $record->cat_id : explode(',',$record->cat_id), $more_categories);
0 ignored issues
show
Bug Best Practice introduced by
The property cat_id does not exist on importexport_iface_egw_record. Since you implemented __get, consider adding a @property annotation.
Loading history...
Bug Best Practice introduced by
The property cat_id does not exist on importexport_iface_egw_record. Since you implemented __set, consider adding a @property annotation.
Loading history...
202
203
		// Private set but missing causes hidden entries
204
		if(array_key_exists('private', $record_array) && (!isset($record_array['private']) || $record_array['private'] == '')) unset($record->private);
0 ignored issues
show
Bug Best Practice introduced by
The property private does not exist on importexport_iface_egw_record. Since you implemented __get, consider adding a @property annotation.
Loading history...
205
206
		// Format birthday as backend requires - converter should give timestamp
207
		if($record->bday && is_numeric($record->bday))
0 ignored issues
show
Bug Best Practice introduced by
The property bday does not exist on importexport_iface_egw_record. Since you implemented __get, consider adding a @property annotation.
Loading history...
208
		{
209
			$time = new Api\DateTime($record->bday);
210
			$record->bday = $time->format('Y-m-d');
0 ignored issues
show
Bug Best Practice introduced by
The property bday does not exist on importexport_iface_egw_record. Since you implemented __set, consider adding a @property annotation.
Loading history...
211
		}
212
213
		if ( $this->definition->plugin_options['conditions'] ) {
214
			foreach ( $this->definition->plugin_options['conditions'] as $condition ) {
215
				$contacts = array();
216
				switch ( $condition['type'] ) {
217
					// exists
218
					case 'exists' :
219
						if($record_array[$condition['string']]) {
220
							$searchcondition = array( $condition['string'] => $record_array[$condition['string']]);
221
							// if we use account_id for the condition, we need to set the owner for filtering, as this
222
							// enables Api\Contacts\Storage to decide what backend is to be used
223
							if ($condition['string']=='account_id') $searchcondition['owner']=0;
224
							$contacts = $this->bocontacts->search(
225
								//array( $condition['string'] => $record[$condition['string']],),
226
								'',
227
								$this->definition->plugin_options['update_cats'] == 'add' ? false : true,
228
								'', '', '', false, 'AND', false,
229
								$searchcondition
230
							);
231
						}
232
						if ( is_array( $contacts ) && count( array_keys( $contacts ) ) >= 1 ) {
233
							// apply action to all contacts matching this exists condition
234
							$action = $condition['true'];
235
							foreach ( (array)$contacts as $contact ) {
236
								$record->id = $contact['id'];
237
								if ( $this->definition->plugin_options['update_cats'] == 'add' ) {
238
									if ( !is_array( $contact['cat_id'] ) ) $contact['cat_id'] = explode( ',', $contact['cat_id'] );
239
									if ( !is_array( $record_array['cat_id'] ) ) $record->cat_id = explode( ',', $record->cat_id );
240
									$record->cat_id = implode( ',', array_unique( array_merge( $record->cat_id, $contact['cat_id'] ) ) );
241
								}
242
								$success = $this->action(  $action['action'], $record, $import_csv->get_current_position() );
243
							}
244
						} else {
245
							$action = $condition['false'];
246
							$success = ($this->action(  $action['action'], $record, $import_csv->get_current_position() ));
247
						}
248
						break;
249
					case 'equal':
250
						// Match on field
251
						$result = $this->equal($record, $condition);
252
						if($result)
253
						{
254
							// Apply true action to any matching records found
255
							$action = $condition['true'];
256
							$success = ($this->action(  $action['action'], $record, $import_csv->get_current_position() ));
257
						}
258
						else
259
						{
260
							// Apply false action if no matching records found
261
							$action = $condition['false'];
262
							$success = ($this->action(  $action['action'], $record, $import_csv->get_current_position() ));
263
						}
264
						break;
265
266
					// not supported action
267
					default :
268
						die('condition / action not supported!!!');
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
269
				}
270
				if ($action['stop']) break;
271
			}
272
		} else {
273
			// unconditional insert
274
			$success = $this->action( 'insert', $record, $import_csv->get_current_position() );
275
		}
276
		return $success;
277
	}
278
279
	/**
280
	 * perform the required action
281
	 *
282
	 * @param int $_action one of $this->actions
283
	 * @param importexport_iface_egw_record $record contact data for the action
284
	 * @return bool success or not
285
	 */
286
	protected function action ( $_action, importexport_iface_egw_record &$record, $record_num = 0 ) {
287
		$_data = $record->get_record_array();
288
289
		// Make sure picture is loaded/updated
290
		if($_data['jpegphoto'])
291
		{
292
			$_data['photo_unchanged'] = false;
293
		}
294
295
		switch ($_action) {
296
			case 'none' :
297
				return true;
298
			case 'delete':
299
				if($_data['id'])
300
				{
301
					if ( $this->dry_run ) {
302
						//print_r($_data);
303
						$this->results[$_action]++;
304
						return true;
305
					}
306
					$result = $this->bocontacts->delete($_data);
307
					if($result && $result === true)
0 ignored issues
show
The condition $result is always false.
Loading history...
308
					{
309
						$this->results[$_action]++;
310
					}
311
					else
312
					{
313
						// Failure of some kind - unknown cause
314
						$this->errors[$record_num] = lang('unable to delete');
315
					}
316
				}
317
				break;
318
			case 'update' :
319
				// Only update if there are changes
320
				$old = $this->bocontacts->read($_data['id']);
321
				// if we get countrycodes as countryname, try to translate them -> the rest should be handled by bo classes.
322
				foreach(array('adr_one_', 'adr_two_') as $c_prefix) {
323
					if (strlen(trim($_data[$c_prefix.'countryname']))==2)
324
						$_data[$c_prefix.'countryname'] = $GLOBALS['egw']->country->get_full_name(trim($_data[$c_prefix.'countryname']), true);
325
				}
326
				// Don't change a user account into a contact
327
				if($old['owner'] == 0) {
328
					unset($_data['owner']);
329
				} elseif(!$this->definition->plugin_options['change_owner']) {
330
					// Don't change addressbook of an existing contact
331
					unset($_data['owner']);
332
				}
333
334
				$this->ids[] = $_data['id'];
335
336
				// Merge to deal with fields not in import record
337
				$_data = array_merge($old, $_data);
338
				$changed = $this->tracking->changed_fields($_data, $old);
339
				if(count($changed) == 0) {
340
					return true;
341
				} else {
342
					//error_log(__METHOD__.__LINE__.array2string($changed).' Old:'.$old['adr_one_countryname'].' ('.$old['adr_one_countrycode'].') New:'.$_data['adr_one_countryname'].' ('.$_data['adr_one_countryname'].')');
343
				}
344
345
				// Make sure n_fn gets updated
346
				unset($_data['n_fn']);
347
348
				// Fall through
349
			case 'insert' :
350
				if($_action == 'insert') {
351
					// Addressbook backend doesn't like inserting with ID specified, it screws up the owner & etag
352
					unset($_data['id']);
353
				}
354
				if(!isset($_data['org_name'])) {
355
					// org_name is a trigger to update n_fileas
356
					$_data['org_name'] = '';
357
				}
358
359
				if ( $this->dry_run ) {
360
					//print_r($_data);
361
					$this->results[$_action]++;
362
					return true;
363
				} else {
364
					$result = $this->bocontacts->save( $_data, $this->is_admin);
365
					if(!$result) {
366
						$this->errors[$record_num] = $this->bocontacts->error;
367
					} else {
368
						$this->ids[] = $result;
369
						$this->results[$_action]++;
370
						// This does nothing (yet?) but update the identifier
371
						$record->save($result);
372
					}
373
					return $result;
374
				}
375
			default:
376
				throw new Api\Exception('Unsupported action: '. $_action);
377
378
		}
379
	}
380
381
382
	/**
383
	 * Delete all contacts from the addressbook, except the given list
384
	 *
385
	 * @param int $addressbook Addressbook to clear
386
	 * @param array $ids Contacts to keep
387
	 */
388
	protected function empty_addressbook($addressbook, $ids)
389
	{
390
		// Get all IDs in addressbook
391
		$contacts = $this->bocontacts->search(array('owner' => $addressbook), true);
392
		$contacts = array_column($contacts, 'id');
393
394
		$delete = array_diff($contacts, $ids);
395
396
		foreach($delete as $id)
397
		{
398
			if($this->dry_run || $this->bocontacts->delete($id))
399
			{
400
				$this->results['deleted']++;
401
			}
402
			else
403
			{
404
				$this->warnings[] = lang('Unable to delete') . ': ' . Api\Link::title('addressbook', $id);
0 ignored issues
show
Bug Best Practice introduced by
The property warnings does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
405
			}
406
		}
407
	}
408
409
	/**
410
	 * returns translated name of plugin
411
	 *
412
	 * @return string name
413
	 */
414
	public static function get_name() {
415
		return lang('Addressbook CSV import');
416
	}
417
418
	/**
419
	 * returns translated (user) description of plugin
420
	 *
421
	 * @return string descriprion
422
	 */
423
	public static function get_description() {
424
		return lang("Imports contacts into your Addressbook from a CSV File. CSV means 'Comma Separated Values'. However in the options Tab you can also choose other seperators.");
425
	}
426
427
	/**
428
	 * retruns file suffix(s) plugin can handle (e.g. csv)
429
	 *
430
	 * @return string suffix (comma seperated)
431
	 */
432
	public static function get_filesuffix() {
433
		return 'csv';
434
	}
435
436
	/**
437
	 * return etemplate components for options.
438
	 * @abstract We can't deal with etemplate objects here, as an uietemplate
439
	 * objects itself are scipt orientated and not "dialog objects"
440
	 *
441
	 * @return array (
442
	 * 		name 		=> string,
443
	 * 		content		=> array,
444
	 * 		sel_options => array,
445
	 * 		preserv		=> array,
446
	 * )
447
	 */
448
	public function get_options_etpl(importexport_definition &$definition=null)
449
	{
450
		// lets do it!
451
	}
452
453
	/**
454
	 * returns etemplate name for slectors of this plugin
455
	 *
456
	 * @return string etemplate name
457
	 */
458
	public function get_selectors_etpl() {
459
		// lets do it!
460
	}
461
462
	/**
463
        * Returns warnings that were encountered during importing
464
        * Maximum of one warning message per record, but you can append if you need to
465
        *
466
        * @return Array (
467
        *       record_# => warning message
468
        *       )
469
        */
470
        public function get_warnings() {
471
		return $this->warnings;
472
	}
473
474
	/**
475
        * Returns errors that were encountered during importing
476
        * Maximum of one error message per record, but you can append if you need to
477
        *
478
        * @return Array (
479
        *       record_# => error message
480
        *       )
481
        */
482
        public function get_errors() {
483
		return $this->errors;
484
	}
485
486
	/**
487
        * Returns a list of actions taken, and the number of records for that action.
488
        * Actions are things like 'insert', 'update', 'delete', and may be different for each plugin.
489
        *
490
        * @return Array (
491
        *       action => record count
492
        * )
493
        */
494
        public function get_results() {
495
                return $this->results;
496
        }
497
}
498