Completed
Push — 16.1 ( a067d8...7d8eb1 )
by Nathan
15:30
created

addressbook_import_contacts_csv   D

Complexity

Total Complexity 81

Size/Duplication

Total Lines 420
Duplicated Lines 19.05 %

Coupling/Cohesion

Components 1
Dependencies 8

Importance

Changes 0
Metric Value
dl 80
loc 420
rs 4.8717
c 0
b 0
f 0
wmc 81
lcom 1
cbo 8

11 Methods

Rating   Name   Duplication   Size   Complexity  
C init() 0 46 9
F import_record() 28 149 46
D action() 52 84 18
A get_name() 0 3 1
A get_description() 0 3 1
A get_filesuffix() 0 3 1
A get_options_etpl() 0 3 1
A get_selectors_etpl() 0 3 1
A get_warnings() 0 3 1
A get_errors() 0 3 1
A get_results() 0 3 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like addressbook_import_contacts_csv 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 addressbook_import_contacts_csv, and based on these observations, apply Extract Interface, too.

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 bocontacts
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
	 * imports entries according to given definition object.
47
	 * @param resource $_stream
48
	 * @param string $_charset
49
	 * @param definition $_definition
50
	 */
51
	public function init(importexport_definition &$_definition ) {
52
53
		// fetch the addressbook bo
54
		$this->bocontacts = new Api\Contacts();
55
56
		// Get the tracker for changes
57
		$this->tracking = new Api\Contacts\Tracking($this->bocontacts);
58
59
		$this->lookups = array(
60
			'tid' => array('n'=>'contact')
61
		);
62
		foreach($this->bocontacts->content_types as $tid => $data)
63
		{
64
			$this->lookups['tid'][$tid] = $data['name'];
65
		}
66
67
		// Try and set a default type, for use if file does not specify
68
		if(!$this->lookups['tid'][Api\Contacts\Storage::DELETED_TYPE] && count($this->lookups['tid']) == 1 ||
69
			$this->lookups['tid'][Api\Contacts\Storage::DELETED_TYPE] && count($this->lookups['tid']) == 2)
70
		{
71
			reset($this->lookups['tid']);
72
			$this->default_type = key($this->lookups['tid']);
73
		}
74
75
76
		// set contact owner
77
		$contact_owner = isset( $_definition->plugin_options['contact_owner'] ) ?
78
			$_definition->plugin_options['contact_owner'] : $this->user;
79
80
		// Check to make sure target addressbook is valid
81
		if(!in_array($contact_owner, array_keys($this->bocontacts->get_addressbooks(Api\Acl::ADD))))
82
		{
83
			$this->warnings[0] = lang("Unable to import into %1, using %2",
84
				$contact_owner . ' ('.Api\Accounts::username($record->owner) . ')',
85
				Api\Accounts::username($this->user)
86
			);
87
			$contact_owner = 'personal';
88
		}
89
90
		// Import into importer's personal addressbook
91
		if($contact_owner == 'personal')
92
		{
93
			$contact_owner = $this->user;
94
		}
95
		$this->user = $contact_owner;
96
	}
97
98
	/**
99
	 * Import a single record
100
	 *
101
	 * You don't need to worry about mappings or translations, they've been done already.
102
	 * You do need to handle the conditions and the actions taken.
103
	 *
104
	 * Updates the count of actions taken
105
	 *
106
	 * @return boolean success
107
	 */
108
	protected function import_record(importexport_iface_egw_record &$record, &$import_csv)
109
	{
110
111
		// Set owner, unless it's supposed to come from CSV file
112
		if($this->definition->plugin_options['owner_from_csv'] && $record->owner) {
113
			if(!is_numeric($record->owner)) {
114
				// Automatically handle text owner without explicit translation
115
				$new_owner = importexport_helper_functions::account_name2id($record->owner);
116
				if($new_owner == '') {
117
					$this->errors[$import_csv->get_current_position()] = lang(
118
						'Unable to convert "%1" to account ID.  Using plugin setting (%2) for owner.',
119
						$record->owner,
120
						Api\Accounts::username($this->user)
121
					);
122
					$record->owner = $this->user;
123
				} else {
124
					$record->owner = $new_owner;
125
				}
126
			}
127
		} else {
128
			$record->owner = $this->user;
129
		}
130
131
		// Check that owner (addressbook) is allowed
132
		if(!array_key_exists($record->owner, $this->bocontacts->get_addressbooks()))
133
		{
134
			$this->errors[$import_csv->get_current_position()] = lang("Unable to import into %1, using %2",
135
				Api\Accounts::username($record->owner),
136
				Api\Accounts::username($this->user)
137
			);
138
			$record->owner = $this->user;
139
		}
140
141
		// Do not allow owner == 0 (accounts) without an account_id
142
		// It causes the contact to be filed as an account, and can't delete
143
		if(!$record->owner && !$record->account_id)
0 ignored issues
show
Bug introduced by
Accessing account_id on the interface importexport_iface_egw_record suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
144
		{
145
			$record->owner = $GLOBALS['egw_info']['user']['account_id'];
146
		}
147
148
		// Do not import into non-existing type, warn and change
149
		if(!$record->tid || !$this->lookups['tid'][$record->tid])
150
		{
151
			// Avoid lots of warnings about type (2 types are contact and deleted)
152
			if($record->tid && !$this->type_warned[$record->tid] && !$this->lookups['tid'][$record->tid] )
0 ignored issues
show
Bug introduced by
Accessing tid on the interface importexport_iface_egw_record suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
153
			{
154
				$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 introduced by
Accessing tid on the interface importexport_iface_egw_record suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
155
				$this->type_warned[$record->tid] = true;
0 ignored issues
show
Bug introduced by
Accessing tid on the interface importexport_iface_egw_record suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
156
			}
157
			$record->tid = $this->default_type;
0 ignored issues
show
Bug introduced by
Accessing tid on the interface importexport_iface_egw_record suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
158
		}
159
160
		// Also handle categories in their own field
161
		$record_array = $record->get_record_array();
162
		$more_categories = array();
163
		foreach($this->definition->plugin_options['field_mapping'] as $field_name) {
164
			if(!array_key_exists($field_name, $record_array) ||
165
				substr($field_name,0,3) != 'cat' || !$record->$field_name || $field_name == 'cat_id') continue;
166
			list(, $cat_id) = explode('-', $field_name);
167
			if(is_numeric($record->$field_name) && $record->$field_name != 1) {
168
				// Column has a single category ID
169
				$more_categories[] = $record->$field_name;
170
			} elseif($record->$field_name == '1' ||
171
				(!is_numeric($record->$field_name) && strtolower($record->$field_name) == strtolower(lang('Yes')))) {
172
				// Each category got its own column.  '1' is the database value, lang('yes') is the human value
173
				$more_categories[] = $cat_id;
174
			} else {
175
				// Text categories
176
				$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));
177
			}
178
		}
179
		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 introduced by
Accessing cat_id on the interface importexport_iface_egw_record suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
180
181
		// Private set but missing causes hidden entries
182
		if(array_key_exists('private', $record_array) && (!isset($record_array['private']) || $record_array['private'] == '')) unset($record->private);
183
184
		// Format birthday as backend requires - converter should give timestamp
185
		if($record->bday && is_numeric($record->bday))
186
		{
187
			$time = new Api\DateTime($record->bday);
188
			$record->bday = $time->format('Y-m-d');
0 ignored issues
show
Bug introduced by
Accessing bday on the interface importexport_iface_egw_record suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
189
		}
190
191
		if ( $this->definition->plugin_options['conditions'] ) {
192
			foreach ( $this->definition->plugin_options['conditions'] as $condition ) {
193
				$contacts = array();
194
				switch ( $condition['type'] ) {
195
					// exists
196
					case 'exists' :
197
						if($record_array[$condition['string']]) {
198
							$searchcondition = array( $condition['string'] => $record_array[$condition['string']]);
199
							// if we use account_id for the condition, we need to set the owner for filtering, as this
200
							// enables Api\Contacts\Storage to decide what backend is to be used
201
							if ($condition['string']=='account_id') $searchcondition['owner']=0;
202
							$contacts = $this->bocontacts->search(
203
								//array( $condition['string'] => $record[$condition['string']],),
204
								'',
205
								$this->definition->plugin_options['update_cats'] == 'add' ? false : true,
206
								'', '', '', false, 'AND', false,
207
								$searchcondition
208
							);
209
						}
210 View Code Duplication
						if ( is_array( $contacts ) && count( array_keys( $contacts ) ) >= 1 ) {
211
							// apply action to all contacts matching this exists condition
212
							$action = $condition['true'];
213
							foreach ( (array)$contacts as $contact ) {
214
								$record->id = $contact['id'];
215
								if ( $this->definition->plugin_options['update_cats'] == 'add' ) {
216
									if ( !is_array( $contact['cat_id'] ) ) $contact['cat_id'] = explode( ',', $contact['cat_id'] );
217
									if ( !is_array( $record_array['cat_id'] ) ) $record->cat_id = explode( ',', $record->cat_id );
218
									$record->cat_id = implode( ',', array_unique( array_merge( $record->cat_id, $contact['cat_id'] ) ) );
219
								}
220
								$success = $this->action(  $action['action'], $record, $import_csv->get_current_position() );
221
							}
222
						} else {
223
							$action = $condition['false'];
224
							$success = ($this->action(  $action['action'], $record, $import_csv->get_current_position() ));
225
						}
226
						break;
227
					case 'equal':
228
						// Match on field
229
						$result = $this->equal($record, $condition);
230 View Code Duplication
						if($result)
231
						{
232
							// Apply true action to any matching records found
233
							$action = $condition['true'];
234
							$success = ($this->action(  $action['action'], $record, $import_csv->get_current_position() ));
235
						}
236
						else
237
						{
238
							// Apply false action if no matching records found
239
							$action = $condition['false'];
240
							$success = ($this->action(  $action['action'], $record, $import_csv->get_current_position() ));
241
						}
242
						break;
243
244
					// not supported action
245
					default :
246
						die('condition / action not supported!!!');
247
						break;
0 ignored issues
show
Unused Code introduced by
break; 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...
248
				}
249
				if ($action['stop']) break;
250
			}
251
		} else {
252
			// unconditional insert
253
			$success = $this->action( 'insert', $record, $import_csv->get_current_position() );
254
		}
255
		return $success;
256
	}
257
258
	/**
259
	 * perform the required action
260
	 *
261
	 * @param int $_action one of $this->actions
262
	 * @param importexport_iface_egw_record $record contact data for the action
263
	 * @return bool success or not
264
	 */
265
	protected function action ( $_action, importexport_iface_egw_record &$record, $record_num = 0 ) {
266
		$_data = $record->get_record_array();
267
		switch ($_action) {
268
			case 'none' :
269
				return true;
270
			case 'delete':
271
				if($_data['id'])
272
				{
273
					if ( $this->dry_run ) {
274
						//print_r($_data);
275
						$this->results[$_action]++;
276
						return true;
277
					}
278
					$result = $this->bocontacts->delete($_data);
279
					if($result && $result === true)
280
					{
281
						$this->results[$_action]++;
282
					}
283
					else
284
					{
285
						// Failure of some kind - unknown cause
286
						$this->errors[$record_num] = lang('unable to delete');
287
					}
288
				}
289
				break;
290 View Code Duplication
			case 'update' :
291
				// Only update if there are changes
292
				$old = $this->bocontacts->read($_data['id']);
293
				// if we get countrycodes as countryname, try to translate them -> the rest should be handled by bo classes.
294
				foreach(array('adr_one_', 'adr_two_') as $c_prefix) {
295
					if (strlen(trim($_data[$c_prefix.'countryname']))==2)
296
						$_data[$c_prefix.'countryname'] = $GLOBALS['egw']->country->get_full_name(trim($_data[$c_prefix.'countryname']), true);
297
				}
298
				// Don't change a user account into a contact
299
				if($old['owner'] == 0) {
300
					unset($_data['owner']);
301
				} elseif(!$this->definition->plugin_options['change_owner']) {
302
					// Don't change addressbook of an existing contact
303
					unset($_data['owner']);
304
				}
305
306
				// Merge to deal with fields not in import record
307
				$_data = array_merge($old, $_data);
308
				$changed = $this->tracking->changed_fields($_data, $old);
309
				if(count($changed) == 0) {
310
					return true;
311
				} else {
0 ignored issues
show
Unused Code introduced by
This else statement is empty and can be removed.

This check looks for the else branches of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These else branches can be removed.

if (rand(1, 6) > 3) {
print "Check failed";
} else {
    //print "Check succeeded";
}

could be turned into

if (rand(1, 6) > 3) {
    print "Check failed";
}

This is much more concise to read.

Loading history...
312
					//error_log(__METHOD__.__LINE__.array2string($changed).' Old:'.$old['adr_one_countryname'].' ('.$old['adr_one_countrycode'].') New:'.$_data['adr_one_countryname'].' ('.$_data['adr_one_countryname'].')');
313
				}
314
315
				// Make sure n_fn gets updated
316
				unset($_data['n_fn']);
317
318
				// Fall through
319 View Code Duplication
			case 'insert' :
320
				if($_action == 'insert') {
321
					// Addressbook backend doesn't like inserting with ID specified, it screws up the owner & etag
322
					unset($_data['id']);
323
				}
324
				if(!isset($_data['org_name'])) {
325
					// org_name is a trigger to update n_fileas
326
					$_data['org_name'] = '';
327
				}
328
329
				if ( $this->dry_run ) {
330
					//print_r($_data);
331
					$this->results[$_action]++;
332
					return true;
333
				} else {
334
					$result = $this->bocontacts->save( $_data, $this->is_admin);
335
					if(!$result) {
336
						$this->errors[$record_num] = $this->bocontacts->error;
337
					} else {
338
						$this->results[$_action]++;
339
						// This does nothing (yet?) but update the identifier
340
						$record->save($result);
341
					}
342
					return $result;
343
				}
344
			default:
345
				throw new Api\Exception('Unsupported action: '. $_action);
346
347
		}
348
	}
349
350
	/**
351
	 * returns translated name of plugin
352
	 *
353
	 * @return string name
354
	 */
355
	public static function get_name() {
356
		return lang('Addressbook CSV import');
357
	}
358
359
	/**
360
	 * returns translated (user) description of plugin
361
	 *
362
	 * @return string descriprion
363
	 */
364
	public static function get_description() {
365
		return lang("Imports contacts into your Addressbook from a CSV File. CSV means 'Comma Seperated Values'. However in the options Tab you can also choose other seperators.");
366
	}
367
368
	/**
369
	 * retruns file suffix(s) plugin can handle (e.g. csv)
370
	 *
371
	 * @return string suffix (comma seperated)
372
	 */
373
	public static function get_filesuffix() {
374
		return 'csv';
375
	}
376
377
	/**
378
	 * return etemplate components for options.
379
	 * @abstract We can't deal with etemplate objects here, as an uietemplate
380
	 * objects itself are scipt orientated and not "dialog objects"
381
	 *
382
	 * @return array (
383
	 * 		name 		=> string,
384
	 * 		content		=> array,
385
	 * 		sel_options => array,
386
	 * 		preserv		=> array,
387
	 * )
388
	 */
389
	public function get_options_etpl() {
390
		// lets do it!
391
	}
392
393
	/**
394
	 * returns etemplate name for slectors of this plugin
395
	 *
396
	 * @return string etemplate name
397
	 */
398
	public function get_selectors_etpl() {
399
		// lets do it!
400
	}
401
402
	/**
403
        * Returns warnings that were encountered during importing
404
        * Maximum of one warning message per record, but you can append if you need to
405
        *
406
        * @return Array (
407
        *       record_# => warning message
408
        *       )
409
        */
410
        public function get_warnings() {
411
		return $this->warnings;
412
	}
413
414
	/**
415
        * Returns errors that were encountered during importing
416
        * Maximum of one error message per record, but you can append if you need to
417
        *
418
        * @return Array (
419
        *       record_# => error message
420
        *       )
421
        */
422
        public function get_errors() {
423
		return $this->errors;
424
	}
425
426
	/**
427
        * Returns a list of actions taken, and the number of records for that action.
428
        * Actions are things like 'insert', 'update', 'delete', and may be different for each plugin.
429
        *
430
        * @return Array (
431
        *       action => record count
432
        * )
433
        */
434
        public function get_results() {
435
                return $this->results;
436
        }
437
}
438