Issues (4868)

inc/class.addressbook_import_vcard.inc.php (11 issues)

1
<?php
2
/**
3
 * importexport plugin to import vCard files
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 Nathan Gray
9
 * @copyright 2012 Nathan Gray
10
 * @version $Id$
11
 */
12
13
use EGroupware\Api;
14
15
/**
16
 * Plugin to import vCard files
17
 */
18
class addressbook_import_vcard implements importexport_iface_import_plugin  {
19
20
	private static $plugin_options = array(
21
22
		'contact_owner',
23
		'charset'
24
	);
25
26
	/**
27
	 * actions wich could be done to data entries
28
	 */
29
	protected static $actions = array('insert');
30
31
	/**
32
	 * conditions for actions
33
	 *
34
	 * @var array
35
	 */
36
	protected static $conditions = array( );
37
38
	/**
39
	 * @var 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...
40
	 */
41
	private $definition;
42
43
	/**
44
	 * @var bocontacts
0 ignored issues
show
The type bocontacts 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...
45
	 */
46
	private $bocontacts;
47
48
	/**
49
	* For figuring out if a contact has changed
50
	*/
51
	protected $tracking;
52
53
	/**
54
	 * @var bool
55
	 */
56
	private $dry_run = false;
57
	private $preview_records = array();
58
59
	/**
60
	 * @var bool is current user admin?
61
	 */
62
	private $is_admin = false;
63
64
	/**
65
	 * @var int
66
	 */
67
	private $user = null;
68
69
	/**
70
	 * List of import warnings
71
	 */
72
	protected $warnings = array();
73
74
	/**
75
	 * List of import errors
76
	 */
77
	protected $errors = array();
78
79
	/**
80
	* List of actions, and how many times that action was taken
81
	*/
82
	protected $results = array();
83
84
	/**
85
	 * imports entries according to given definition object.
86
	 * @param resource $_stream
87
	 * @param string $_charset
88
	 * @param definition $_definition
89
	 */
90
	public function import( $_stream, importexport_definition $_definition ) {
91
		$this->definition = $_definition;
0 ignored issues
show
Documentation Bug introduced by
It seems like $_definition of type importexport_definition is incompatible with the declared type definition of property $definition.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
92
93
		// user, is admin ?
94
		$this->is_admin = isset( $GLOBALS['egw_info']['user']['apps']['admin'] ) && $GLOBALS['egw_info']['user']['apps']['admin'];
95
		$this->user = $GLOBALS['egw_info']['user']['account_id'];
96
97
                // set contact owner
98
                $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...
99
                        $_definition->plugin_options['contact_owner'] : $this->user;
100
                // Import into importer's personal addressbook
101
                if($contact_owner == 'personal')
102
                {
103
                        $contact_owner = $this->user;
104
                }
105
106
		// dry run?
107
		$this->dry_run = isset( $_definition->plugin_options['dry_run'] ) ? $_definition->plugin_options['dry_run'] :  false;
108
109
		// Needed for categories to work right
110
		$GLOBALS['egw_info']['flags']['currentapp'] = 'addressbook';
111
112
		// fetch the addressbook bo
113
		$this->bocontacts = new addressbook_vcal();
0 ignored issues
show
Documentation Bug introduced by
It seems like new addressbook_vcal() of type addressbook_vcal is incompatible with the declared type bocontacts of property $bocontacts.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
114
115
		$charset = $_definition->plugin_options['charset'];
116
		if($charset == 'user') $charset = $GLOBALS['egw_info']['user']['preferences']['addressbook']['vcard_charset'];
117
118
		// Start counting successes
119
		$this->current = 0;
0 ignored issues
show
Bug Best Practice introduced by
The property current does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
120
		$count = 0;
121
		$this->results = array();
122
123
		// Failures
124
		$this->errors = array();
125
126
		// Fix for Apple Addressbook
127
        $vCard = preg_replace('/item\d\.(ADR|TEL|EMAIL|URL)/', '\1', stream_get_contents($_stream));
128
129
		$contacts = new Api\CalDAV\IcalIterator($vCard, '', $charset, array($this, '_vcard'),array(
130
			// Owner (addressbook)
131
			$contact_owner
132
		));
133
		$contacts->next();
134
		while($contacts->valid()) {
135
			$this->current++;
136
			$contact = $contacts->current();
137
138
			$this->action('insert', $contact, $this->current);
139
140
			// Stop if we have enough records for a preview
141
			if($this->dry_run)
142
			{
143
				$egw_record = new addressbook_egw_record();
144
				$egw_record->set_record($contact);
145
				$this->preview_records[] = $egw_record;
146
				if($count >= $GLOBALS['egw_info']['user']['preferences']['common']['maxmatchs']) break;
147
			}
148
149
			$count++;
150
			$contacts->next();
151
		}
152
153
		return $count;
154
	}
155
156
	/**
157
	 * Changes a vcard object into egw data array
158
	 */
159
	public function _vcard($_vcard, $owner)
160
	{
161
		$charset = $this->definition->plugin_options['charset'];
162
		if($charset == 'user') $charset = $GLOBALS['egw_info']['user']['preferences']['addressbook']['vcard_charset'];
163
		$record = $this->bocontacts->vcardtoegw($_vcard,$charset);
164
165
		$record['owner'] = $owner;
166
167
		// Check that owner (addressbook) is allowed
168
		if(!array_key_exists($record['owner'], $this->bocontacts->get_addressbooks()))
169
		{
170
			$this->errors[$this->current] = lang("Unable to import into %1, using %2",
171
				Api\Accounts::username($record['owner']),
0 ignored issues
show
The call to lang() has too many arguments starting with EGroupware\Api\Accounts:...rname($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

171
			$this->errors[$this->current] = /** @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...
172
				Api\Accounts::username($this->user)
173
			);
174
			$record['owner'] = $this->user;
175
		}
176
177
		// Do not allow owner == 0 (accounts) without an account_id
178
		// It causes the contact to be filed as an account, and can't delete
179
		if(!$record['owner'] && !$record['account_id'])
180
		{
181
			$record['owner'] = $this->user;
182
		}
183
184
		// Check & apply value overrides
185
		foreach((array)$this->definition->plugin_options['override_values'] as $field => $settings)
186
		{
187
			if($settings['value'])
188
			{
189
				$record[$field] = $settings['value'];
190
			}
191
		}
192
		if (is_array($record['cat_id']))
193
		{
194
			$record['cat_id'] = implode(',',$this->bocontacts->find_or_add_categories($record['cat_id'], -1));
195
		}
196
		// Make sure picture is loaded/updated
197
		if($record['jpegphoto'])
198
		{
199
			$record['photo_unchanged'] = false;
200
		}
201
		return $record;
202
	}
203
204
	/**
205
	 * perform the required action
206
	 *
207
	 * @param int $_action one of $this->actions
208
	 * @param array $_data contact data for the action
209
	 * @return bool success or not
210
	 */
211
	private function action ( $_action, $_data, $record_num = 0 ) {
212
		switch ($_action) {
213
			case 'none' :
214
				return true;
215
			case 'update' :
216
				// Only update if there are changes
217
				$old = $this->bocontacts->read($_data['id']);
218
				// if we get countrycodes as countryname, try to translate them -> the rest should be handled by bo classes.
219
				foreach(array('adr_one_', 'adr_two_') as $c_prefix)
220
				{
221
					if (strlen(trim($_data[$c_prefix.'countryname']))==2)
222
					{
223
						$_data[$c_prefix.'countryname'] = Api\Country::get_full_name(trim($_data[$c_prefix.'countryname']), true);
224
					}
225
				}
226
				// Don't change a user account into a contact
227
				if($old['owner'] == 0) {
228
					unset($_data['owner']);
229
				} elseif(!$this->definition->plugin_options['change_owner']) {
230
					// Don't change addressbook of an existing contact
231
					unset($_data['owner']);
232
				}
233
234
				// Merge to deal with fields not in import record
235
				$_data = array_merge($old, $_data);
236
				$changed = $this->tracking->changed_fields($_data, $old);
237
				if(count($changed) == 0) {
238
					return true;
239
				} else {
240
					//error_log(__METHOD__.__LINE__.array2string($changed).' Old:'.$old['adr_one_countryname'].' ('.$old['adr_one_countrycode'].') New:'.$_data['adr_one_countryname'].' ('.$_data['adr_one_countryname'].')');
241
				}
242
243
				// Make sure n_fn gets updated
244
				unset($_data['n_fn']);
245
246
				// Fall through
247
			case 'insert' :
248
				if($_action == 'insert') {
249
					// Addressbook backend doesn't like inserting with ID specified, it screws up the owner & etag
250
					unset($_data['id']);
251
				}
252
				if(!isset($_data['org_name'])) {
253
					// org_name is a trigger to update n_fileas
254
					$_data['org_name'] = '';
255
				}
256
257
				if ( $this->dry_run ) {
258
					//print_r($_data);
259
					$this->results[$_action]++;
260
					return true;
261
				} else {
262
					$result = $this->bocontacts->save( $_data, $this->is_admin);
263
					if(!$result) {
264
						$this->errors[$record_num] = $this->bocontacts->error;
265
					} else {
266
						$this->results[$_action]++;
267
					}
268
					return $result;
269
				}
270
			default:
271
				throw new Api\Exception('Unsupported action: '. $_action);
272
273
		}
274
	}
275
276
	public function preview( $_stream, importexport_definition $_definition )
277
	{
278
		$rows = array('h1'=>array(),'f1'=>array(),'.h1'=>'class=th');
279
280
		// Set this so plugin doesn't do any data changes
281
		$_definition->plugin_options = (array)$_definition->plugin_options + array('dry_run' => true);
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...
Bug Best Practice introduced by
The property plugin_options does not exist on importexport_definition. Since you implemented __set, consider adding a @property annotation.
Loading history...
282
283
		$this->import($_stream, $_definition);
284
		rewind($_stream);
285
286
		// Get field labels
287
		$rows['h1'] = $labels = $this->bocontacts->contact_fields;
288
289
		$record_class = get_class($this->preview_records[0]);
290
291
		foreach($this->preview_records as $record)
292
		{
293
			// Convert to human-friendly
294
            importexport_export_csv::convert($record,$record_class::$types,$_definition->application);
0 ignored issues
show
Bug Best Practice introduced by
The property application does not exist on importexport_definition. Since you implemented __get, consider adding a @property annotation.
Loading history...
295
			$record = $record->get_record_array();
296
			$row = array();
297
			foreach(array_keys($labels) as $field)
298
			{
299
				$row[$field] = $record[$field];
300
301
				// Don't scare users, do something with jpeg
302
				if($field == 'jpegphoto' && $row[$field])
303
				{
304
					$row[$field] = '<img style="max-width:50px;max-height:50px;" src="data:image/jpeg;base64,'.$row[$field].'"/>';
305
				}
306
				unset($record[$field]);
307
			}
308
			$row += $record;
309
			$rows[] = $row;
310
		}
311
		return Api\Html::table($rows);
312
	}
313
314
	/**
315
	 * returns translated name of plugin
316
	 *
317
	 * @return string name
318
	 */
319
	public static function get_name() {
320
		return lang('Addressbook vCard import');
321
	}
322
323
	/**
324
	 * returns translated (user) description of plugin
325
	 *
326
	 * @return string descriprion
327
	 */
328
	public static function get_description() {
329
		return lang("Imports contacts into your Addressbook from a vCard File. ");
330
	}
331
332
	/**
333
	 * retruns file suffix(s) plugin can handle (e.g. csv)
334
	 *
335
	 * @return string suffix (comma seperated)
336
	 */
337
	public static function get_filesuffix() {
338
		return 'vcf';
339
	}
340
341
	/**
342
	 * return etemplate components for options.
343
	 * @abstract We can't deal with etemplate objects here, as an uietemplate
344
	 * objects itself are scipt orientated and not "dialog objects"
345
	 *
346
	 * @return array (
347
	 * 		name 		=> string,
348
	 * 		content		=> array,
349
	 * 		sel_options => array,
350
	 * 		preserv		=> array,
351
	 * )
352
	 */
353
	public function get_options_etpl(importexport_definition &$definition=null)
354
	{
355
		$charset = $definition->plugin_options['charset'];
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...
356
		if($charset == 'user') $charset = $GLOBALS['egw_info']['user']['preferences']['addressbook']['vcard_charset'];
357
		return array(
358
			'name' => 'addressbook.import_vcard',
359
			'content' => array(
360
				'file_type' => 'vcard,ical,vcf',
361
				'charset' => $charset
362
			),
363
			'sel_options' => array(
364
				'charset' => Api\Translation::get_installed_charsets()
365
			),
366
			'preserv' => array()
367
		);
368
	}
369
370
	/**
371
	 * returns etemplate name for slectors of this plugin
372
	 *
373
	 * @return string etemplate name
374
	 */
375
	public function get_selectors_etpl() {
376
		// lets do it!
377
	}
378
379
	/**
380
        * Returns warnings that were encountered during importing
381
        * Maximum of one warning message per record, but you can append if you need to
382
        *
383
        * @return Array (
384
        *       record_# => warning message
385
        *       )
386
        */
387
        public function get_warnings() {
388
		return $this->warnings;
389
	}
390
391
	/**
392
        * Returns errors that were encountered during importing
393
        * Maximum of one error message per record, but you can append if you need to
394
        *
395
        * @return Array (
396
        *       record_# => error message
397
        *       )
398
        */
399
        public function get_errors() {
400
		return $this->errors;
401
	}
402
403
	/**
404
        * Returns a list of actions taken, and the number of records for that action.
405
        * Actions are things like 'insert', 'update', 'delete', and may be different for each plugin.
406
        *
407
        * @return Array (
408
        *       action => record count
409
        * )
410
        */
411
        public function get_results() {
412
                return $this->results;
413
        }
414
}
415