~josephjamesmills/zpanelcp/zpanelcp

« back to all changes in this revision

Viewing changes to backups/zpanelx-10-zpanelx/etc/apps/webmail/program/include/rcube_vcard.php

  • Committer: Joseph Mills
  • Date: 2012-05-12 06:38:23 UTC
  • Revision ID: josephjamesmills@gmail.com-20120512063823-nnb5w44xdkkbg8ds
made new framework and got ride of the backupfiles fixed amny of the bugs or tried too at least. added steps tpwards making ssl by default. Fixed apache virtual host files and moved to the right area. fixed all things too go under /var/www. and not /etc/. changed all the persission so that no one can read all files.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
<?php
2
 
 
3
 
/*
4
 
 +-----------------------------------------------------------------------+
5
 
 | program/include/rcube_vcard.php                                       |
6
 
 |                                                                       |
7
 
 | This file is part of the Roundcube Webmail client                     |
8
 
 | Copyright (C) 2008-2011, The Roundcube Dev Team                       |
9
 
 | Licensed under the GNU GPL                                            |
10
 
 |                                                                       |
11
 
 | PURPOSE:                                                              |
12
 
 |   Logical representation of a vcard address record                    |
13
 
 +-----------------------------------------------------------------------+
14
 
 | Author: Thomas Bruederli <roundcube@gmail.com>                        |
15
 
 +-----------------------------------------------------------------------+
16
 
 
17
 
 $Id: rcube_vcard.php 5160 2011-09-05 07:40:18Z thomasb $
18
 
 
19
 
*/
20
 
 
21
 
 
22
 
/**
23
 
 * Logical representation of a vcard-based address record
24
 
 * Provides functions to parse and export vCard data format
25
 
 *
26
 
 * @package    Addressbook
27
 
 * @author     Thomas Bruederli <roundcube@gmail.com>
28
 
 */
29
 
class rcube_vcard
30
 
{
31
 
  private static $values_decoded = false;
32
 
  private $raw = array(
33
 
    'FN' => array(),
34
 
    'N' => array(array('','','','','')),
35
 
  );
36
 
  private static $fieldmap = array(
37
 
    'phone'    => 'TEL',
38
 
    'birthday' => 'BDAY',
39
 
    'website'  => 'URL',
40
 
    'notes'    => 'NOTE',
41
 
    'email'    => 'EMAIL',
42
 
    'address'  => 'ADR',
43
 
    'jobtitle' => 'TITLE',
44
 
    'department'  => 'X-DEPARTMENT',
45
 
    'gender'      => 'X-GENDER',
46
 
    'maidenname'  => 'X-MAIDENNAME',
47
 
    'anniversary' => 'X-ANNIVERSARY',
48
 
    'assistant'   => 'X-ASSISTANT',
49
 
    'manager'     => 'X-MANAGER',
50
 
    'spouse'      => 'X-SPOUSE',
51
 
    'edit'        => 'X-AB-EDIT',
52
 
  );
53
 
  private $typemap = array('iPhone' => 'mobile', 'CELL' => 'mobile', 'WORK,FAX' => 'workfax');
54
 
  private $phonetypemap = array('HOME1' => 'HOME', 'BUSINESS1' => 'WORK', 'BUSINESS2' => 'WORK2', 'BUSINESSFAX' => 'WORK,FAX');
55
 
  private $addresstypemap = array('BUSINESS' => 'WORK');
56
 
  private $immap = array('X-JABBER' => 'jabber', 'X-ICQ' => 'icq', 'X-MSN' => 'msn', 'X-AIM' => 'aim', 'X-YAHOO' => 'yahoo', 'X-SKYPE' => 'skype', 'X-SKYPE-USERNAME' => 'skype');
57
 
 
58
 
  public $business = false;
59
 
  public $displayname;
60
 
  public $surname;
61
 
  public $firstname;
62
 
  public $middlename;
63
 
  public $nickname;
64
 
  public $organization;
65
 
  public $notes;
66
 
  public $email = array();
67
 
 
68
 
  public static $eol = "\r\n";
69
 
 
70
 
  /**
71
 
   * Constructor
72
 
   */
73
 
  public function __construct($vcard = null, $charset = RCMAIL_CHARSET, $detect = false, $fieldmap = array())
74
 
  {
75
 
    if (!empty($fielmap))
76
 
      $this->extend_fieldmap($fieldmap);
77
 
 
78
 
    if (!empty($vcard))
79
 
      $this->load($vcard, $charset, $detect);
80
 
  }
81
 
 
82
 
 
83
 
  /**
84
 
   * Load record from (internal, unfolded) vcard 3.0 format
85
 
   *
86
 
   * @param string vCard string to parse
87
 
   * @param string Charset of string values
88
 
   * @param boolean True if loading a 'foreign' vcard and extra heuristics for charset detection is required
89
 
   */
90
 
  public function load($vcard, $charset = RCMAIL_CHARSET, $detect = false)
91
 
  {
92
 
    self::$values_decoded = false;
93
 
    $this->raw = self::vcard_decode($vcard);
94
 
 
95
 
    // resolve charset parameters
96
 
    if ($charset == null) {
97
 
      $this->raw = self::charset_convert($this->raw);
98
 
    }
99
 
    // vcard has encoded values and charset should be detected
100
 
    else if ($detect && self::$values_decoded &&
101
 
      ($detected_charset = self::detect_encoding(self::vcard_encode($this->raw))) && $detected_charset != RCMAIL_CHARSET) {
102
 
        $this->raw = self::charset_convert($this->raw, $detected_charset);
103
 
    }
104
 
    
105
 
    // consider FN empty if the same as the primary e-mail address
106
 
    if ($this->raw['FN'][0][0] == $this->raw['EMAIL'][0][0])
107
 
      $this->raw['FN'][0][0] = '';
108
 
 
109
 
    // find well-known address fields
110
 
    $this->displayname = $this->raw['FN'][0][0];
111
 
    $this->surname = $this->raw['N'][0][0];
112
 
    $this->firstname = $this->raw['N'][0][1];
113
 
    $this->middlename = $this->raw['N'][0][2];
114
 
    $this->nickname = $this->raw['NICKNAME'][0][0];
115
 
    $this->organization = $this->raw['ORG'][0][0];
116
 
    $this->business = ($this->raw['X-ABSHOWAS'][0][0] == 'COMPANY') || (join('', (array)$this->raw['N'][0]) == '' && !empty($this->organization));
117
 
 
118
 
    foreach ((array)$this->raw['EMAIL'] as $i => $raw_email)
119
 
      $this->email[$i] = is_array($raw_email) ? $raw_email[0] : $raw_email;
120
 
 
121
 
    // make the pref e-mail address the first entry in $this->email
122
 
    $pref_index = $this->get_type_index('EMAIL', 'pref');
123
 
    if ($pref_index > 0) {
124
 
      $tmp = $this->email[0];
125
 
      $this->email[0] = $this->email[$pref_index];
126
 
      $this->email[$pref_index] = $tmp;
127
 
    }
128
 
  }
129
 
 
130
 
 
131
 
  /**
132
 
   * Return vCard data as associative array to be unsed in Roundcube address books
133
 
   *
134
 
   * @return array Hash array with key-value pairs
135
 
   */
136
 
  public function get_assoc()
137
 
  {
138
 
    $out = array('name' => $this->displayname);
139
 
    $typemap = $this->typemap;
140
 
 
141
 
    // copy name fields to output array
142
 
    foreach (array('firstname','surname','middlename','nickname','organization') as $col) {
143
 
      if (strlen($this->$col))
144
 
        $out[$col] = $this->$col;
145
 
    }
146
 
 
147
 
    if ($this->raw['N'][0][3])
148
 
      $out['prefix'] = $this->raw['N'][0][3];
149
 
    if ($this->raw['N'][0][4])
150
 
      $out['suffix'] = $this->raw['N'][0][4];
151
 
 
152
 
    // convert from raw vcard data into associative data for Roundcube
153
 
    foreach (array_flip(self::$fieldmap) as $tag => $col) {
154
 
      foreach ((array)$this->raw[$tag] as $i => $raw) {
155
 
        if (is_array($raw)) {
156
 
          $k = -1;
157
 
          $key = $col;
158
 
          $subtype = '';
159
 
 
160
 
          if (!empty($raw['type'])) {
161
 
            $combined = join(',', self::array_filter((array)$raw['type'], 'internet,pref', true));
162
 
            $subtype = $typemap[$combined] ? $typemap[$combined] : ($typemap[$raw['type'][++$k]] ? $typemap[$raw['type'][$k]] : strtolower($raw['type'][$k]));
163
 
            while ($k < count($raw['type']) && ($subtype == 'internet' || $subtype == 'pref'))
164
 
              $subtype = $typemap[$raw['type'][++$k]] ? $typemap[$raw['type'][$k]] : strtolower($raw['type'][$k]);
165
 
          }
166
 
 
167
 
          // read vcard 2.1 subtype
168
 
          if (!$subtype) {
169
 
            foreach ($raw as $k => $v) {
170
 
              if (!is_numeric($k) && $v === true && !in_array(strtolower($k), array('pref','internet','voice','base64'))) {
171
 
                $subtype = $typemap[$k] ? $typemap[$k] : strtolower($k);
172
 
                break;
173
 
              }
174
 
            }
175
 
          }
176
 
 
177
 
          // force subtype if none set
178
 
          if (!$subtype && preg_match('/^(email|phone|address|website)/', $key))
179
 
            $subtype = 'other';
180
 
 
181
 
          if ($subtype)
182
 
            $key .= ':' . $subtype;
183
 
 
184
 
          // split ADR values into assoc array
185
 
          if ($tag == 'ADR') {
186
 
            list(,, $value['street'], $value['locality'], $value['region'], $value['zipcode'], $value['country']) = $raw;
187
 
            $out[$key][] = $value;
188
 
          }
189
 
          else
190
 
            $out[$key][] = $raw[0];
191
 
        }
192
 
        else {
193
 
          $out[$col][] = $raw;
194
 
        }
195
 
      }
196
 
    }
197
 
 
198
 
    // handle special IM fields as used by Apple
199
 
    foreach ($this->immap as $tag => $type) {
200
 
      foreach ((array)$this->raw[$tag] as $i => $raw) {
201
 
        $out['im:'.$type][] = $raw[0];
202
 
      }
203
 
    }
204
 
 
205
 
    // copy photo data
206
 
    if ($this->raw['PHOTO'])
207
 
      $out['photo'] = $this->raw['PHOTO'][0][0];
208
 
 
209
 
    return $out;
210
 
  }
211
 
 
212
 
 
213
 
  /**
214
 
   * Convert the data structure into a vcard 3.0 string
215
 
   */
216
 
  public function export($folded = true)
217
 
  {
218
 
    $vcard = self::vcard_encode($this->raw);
219
 
    return $folded ? self::rfc2425_fold($vcard) : $vcard;
220
 
  }
221
 
 
222
 
 
223
 
  /**
224
 
   * Clear the given fields in the loaded vcard data
225
 
   *
226
 
   * @param array List of field names to be reset
227
 
   */
228
 
  public function reset($fields = null)
229
 
  {
230
 
    if (!$fields)
231
 
      $fields = array_merge(array_values(self::$fieldmap), array_keys($this->immap), array('FN','N','ORG','NICKNAME','EMAIL','ADR','BDAY'));
232
 
 
233
 
    foreach ($fields as $f)
234
 
      unset($this->raw[$f]);
235
 
 
236
 
    if (!$this->raw['N'])
237
 
      $this->raw['N'] = array(array('','','','',''));
238
 
    if (!$this->raw['FN'])
239
 
      $this->raw['FN'] = array();
240
 
 
241
 
    $this->email = array();
242
 
  }
243
 
 
244
 
 
245
 
  /**
246
 
   * Setter for address record fields
247
 
   *
248
 
   * @param string Field name
249
 
   * @param string Field value
250
 
   * @param string Type/section name
251
 
   */
252
 
  public function set($field, $value, $type = 'HOME')
253
 
  {
254
 
    $field = strtolower($field);
255
 
    $type_uc = strtoupper($type);
256
 
    $typemap = array_flip($this->typemap);
257
 
 
258
 
    switch ($field) {
259
 
      case 'name':
260
 
      case 'displayname':
261
 
        $this->raw['FN'][0][0] = $value;
262
 
        break;
263
 
 
264
 
      case 'surname':
265
 
        $this->raw['N'][0][0] = $value;
266
 
        break;
267
 
 
268
 
      case 'firstname':
269
 
        $this->raw['N'][0][1] = $value;
270
 
        break;
271
 
 
272
 
      case 'middlename':
273
 
        $this->raw['N'][0][2] = $value;
274
 
        break;
275
 
 
276
 
      case 'prefix':
277
 
        $this->raw['N'][0][3] = $value;
278
 
        break;
279
 
 
280
 
      case 'suffix':
281
 
        $this->raw['N'][0][4] = $value;
282
 
        break;
283
 
 
284
 
      case 'nickname':
285
 
        $this->raw['NICKNAME'][0][0] = $value;
286
 
        break;
287
 
 
288
 
      case 'organization':
289
 
        $this->raw['ORG'][0][0] = $value;
290
 
        break;
291
 
 
292
 
      case 'photo':
293
 
        if (strpos($value, 'http:') === 0) {
294
 
            // TODO: fetch file from URL and save it locally?
295
 
            $this->raw['PHOTO'][0] = array(0 => $value, 'URL' => true);
296
 
        }
297
 
        else {
298
 
            $encoded = !preg_match('![^a-z0-9/=+-]!i', $value);
299
 
            $this->raw['PHOTO'][0] = array(0 => $encoded ? $value : base64_encode($value), 'BASE64' => true);
300
 
        }
301
 
        break;
302
 
 
303
 
      case 'email':
304
 
        $this->raw['EMAIL'][] = array(0 => $value, 'type' => array_filter(array('INTERNET', $type_uc)));
305
 
        $this->email[] = $value;
306
 
        break;
307
 
 
308
 
      case 'im':
309
 
        // save IM subtypes into extension fields
310
 
        $typemap = array_flip($this->immap);
311
 
        if ($field = $typemap[strtolower($type)])
312
 
          $this->raw[$field][] = array(0 => $value);
313
 
        break;
314
 
 
315
 
      case 'birthday':
316
 
        if ($val = rcube_strtotime($value))
317
 
          $this->raw['BDAY'][] = array(0 => date('Y-m-d', $val), 'value' => array('date'));
318
 
        break;
319
 
 
320
 
      case 'address':
321
 
        if ($this->addresstypemap[$type_uc])
322
 
          $type = $this->addresstypemap[$type_uc];
323
 
 
324
 
        $value = $value[0] ? $value : array('', '', $value['street'], $value['locality'], $value['region'], $value['zipcode'], $value['country']);
325
 
 
326
 
        // fall through if not empty
327
 
        if (!strlen(join('', $value)))
328
 
          break;
329
 
 
330
 
      default:
331
 
        if ($field == 'phone' && $this->phonetypemap[$type_uc])
332
 
          $type = $this->phonetypemap[$type_uc];
333
 
 
334
 
        if (($tag = self::$fieldmap[$field]) && (is_array($value) || strlen($value))) {
335
 
          $index = count($this->raw[$tag]);
336
 
          $this->raw[$tag][$index] = (array)$value;
337
 
          if ($type)
338
 
            $this->raw[$tag][$index]['type'] = explode(',', ($typemap[$type] ? $typemap[$type] : $type));
339
 
        }
340
 
        break;
341
 
    }
342
 
  }
343
 
 
344
 
  /**
345
 
   * Setter for individual vcard properties
346
 
   *
347
 
   * @param string VCard tag name
348
 
   * @param array Value-set of this vcard property
349
 
   * @param boolean Set to true if the value-set should be appended instead of replacing any existing value-set
350
 
   */
351
 
  public function set_raw($tag, $value, $append = false)
352
 
  {
353
 
    $index = $append ? count($this->raw[$tag]) : 0;
354
 
    $this->raw[$tag][$index] = (array)$value;
355
 
  }
356
 
 
357
 
 
358
 
  /**
359
 
   * Find index with the '$type' attribute
360
 
   *
361
 
   * @param string Field name
362
 
   * @return int Field index having $type set
363
 
   */
364
 
  private function get_type_index($field, $type = 'pref')
365
 
  {
366
 
    $result = 0;
367
 
    if ($this->raw[$field]) {
368
 
      foreach ($this->raw[$field] as $i => $data) {
369
 
        if (is_array($data['type']) && in_array_nocase('pref', $data['type']))
370
 
          $result = $i;
371
 
      }
372
 
    }
373
 
 
374
 
    return $result;
375
 
  }
376
 
 
377
 
 
378
 
  /**
379
 
   * Convert a whole vcard (array) to UTF-8.
380
 
   * If $force_charset is null, each member value that has a charset parameter will be converted
381
 
   */
382
 
  private static function charset_convert($card, $force_charset = null)
383
 
  {
384
 
    foreach ($card as $key => $node) {
385
 
      foreach ($node as $i => $subnode) {
386
 
        if (is_array($subnode) && (($charset = $force_charset) || ($subnode['charset'] && ($charset = $subnode['charset'][0])))) {
387
 
          foreach ($subnode as $j => $value) {
388
 
            if (is_numeric($j) && is_string($value))
389
 
              $card[$key][$i][$j] = rcube_charset_convert($value, $charset);
390
 
          }
391
 
          unset($card[$key][$i]['charset']);
392
 
        }
393
 
      }
394
 
    }
395
 
 
396
 
    return $card;
397
 
  }
398
 
 
399
 
 
400
 
  /**
401
 
   * Extends fieldmap definition
402
 
   */
403
 
  public function extend_fieldmap($map)
404
 
  {
405
 
    if (is_array($map))
406
 
      self::$fieldmap = array_merge($map, self::$fieldmap);
407
 
  }
408
 
 
409
 
 
410
 
  /**
411
 
   * Factory method to import a vcard file
412
 
   *
413
 
   * @param string vCard file content
414
 
   * @return array List of rcube_vcard objects
415
 
   */
416
 
  public static function import($data)
417
 
  {
418
 
    $out = array();
419
 
 
420
 
    // check if charsets are specified (usually vcard version < 3.0 but this is not reliable)
421
 
    if (preg_match('/charset=/i', substr($data, 0, 2048)))
422
 
      $charset = null;
423
 
    // detect charset and convert to utf-8
424
 
    else if (($charset = self::detect_encoding($data)) && $charset != RCMAIL_CHARSET) {
425
 
      $data = rcube_charset_convert($data, $charset);
426
 
      $data = preg_replace(array('/^[\xFE\xFF]{2}/', '/^\xEF\xBB\xBF/', '/^\x00+/'), '', $data); // also remove BOM
427
 
      $charset = RCMAIL_CHARSET;
428
 
    }
429
 
 
430
 
    $vcard_block = '';
431
 
    $in_vcard_block = false;
432
 
 
433
 
    foreach (preg_split("/[\r\n]+/", $data) as $i => $line) {
434
 
      if ($in_vcard_block && !empty($line))
435
 
        $vcard_block .= $line . "\n";
436
 
 
437
 
      $line = trim($line);
438
 
 
439
 
      if (preg_match('/^END:VCARD$/i', $line)) {
440
 
        // parse vcard
441
 
        $obj = new rcube_vcard(self::cleanup($vcard_block), $charset, true, self::$fieldmap);
442
 
        if (!empty($obj->displayname) || !empty($obj->email))
443
 
          $out[] = $obj;
444
 
 
445
 
        $in_vcard_block = false;
446
 
      }
447
 
      else if (preg_match('/^BEGIN:VCARD$/i', $line)) {
448
 
        $vcard_block = $line . "\n";
449
 
        $in_vcard_block = true;
450
 
      }
451
 
    }
452
 
 
453
 
    return $out;
454
 
  }
455
 
 
456
 
 
457
 
  /**
458
 
   * Normalize vcard data for better parsing
459
 
   *
460
 
   * @param string vCard block
461
 
   * @return string Cleaned vcard block
462
 
   */
463
 
  private static function cleanup($vcard)
464
 
  {
465
 
    // Convert special types (like Skype) to normal type='skype' classes with this simple regex ;)
466
 
    $vcard = preg_replace(
467
 
      '/item(\d+)\.(TEL|EMAIL|URL)([^:]*?):(.*?)item\1.X-ABLabel:(?:_\$!<)?([\w-() ]*)(?:>!\$_)?./s',
468
 
      '\2;type=\5\3:\4',
469
 
      $vcard);
470
 
 
471
 
    // convert Apple X-ABRELATEDNAMES into X-* fields for better compatibility
472
 
    $vcard = preg_replace_callback(
473
 
      '/item(\d+)\.(X-ABRELATEDNAMES)([^:]*?):(.*?)item\1.X-ABLabel:(?:_\$!<)?([\w-() ]*)(?:>!\$_)?./s',
474
 
      array('self', 'x_abrelatednames_callback'),
475
 
      $vcard);
476
 
 
477
 
    // Remove cruft like item1.X-AB*, item1.ADR instead of ADR, and empty lines
478
 
    $vcard = preg_replace(array('/^item\d*\.X-AB.*$/m', '/^item\d*\./m', "/\n+/"), array('', '', "\n"), $vcard);
479
 
 
480
 
    // convert X-WAB-GENDER to X-GENDER
481
 
    if (preg_match('/X-WAB-GENDER:(\d)/', $vcard, $matches)) {
482
 
      $value = $matches[1] == '2' ? 'male' : 'female';
483
 
      $vcard = preg_replace('/X-WAB-GENDER:\d/', 'X-GENDER:' . $value, $vcard);
484
 
    }
485
 
 
486
 
    // if N doesn't have any semicolons, add some 
487
 
    $vcard = preg_replace('/^(N:[^;\R]*)$/m', '\1;;;;', $vcard);
488
 
 
489
 
    return $vcard;
490
 
  }
491
 
 
492
 
  private static function x_abrelatednames_callback($matches)
493
 
  {
494
 
    return 'X-' . strtoupper($matches[5]) . $matches[3] . ':'. $matches[4];
495
 
  }
496
 
 
497
 
  private static function rfc2425_fold_callback($matches)
498
 
  {
499
 
    // chunk_split string and avoid lines breaking multibyte characters
500
 
    $c = 71;
501
 
    $out .= substr($matches[1], 0, $c);
502
 
    for ($n = $c; $c < strlen($matches[1]); $c++) {
503
 
      // break if length > 75 or mutlibyte character starts after position 71
504
 
      if ($n > 75 || ($n > 71 && ord($matches[1][$c]) >> 6 == 3)) {
505
 
        $out .= "\r\n ";
506
 
        $n = 0;
507
 
      }
508
 
      $out .= $matches[1][$c];
509
 
      $n++;
510
 
    }
511
 
 
512
 
    return $out;
513
 
  }
514
 
 
515
 
  public static function rfc2425_fold($val)
516
 
  {
517
 
    return preg_replace_callback('/([^\n]{72,})/', array('self', 'rfc2425_fold_callback'), $val);
518
 
  }
519
 
 
520
 
 
521
 
  /**
522
 
   * Decodes a vcard block (vcard 3.0 format, unfolded)
523
 
   * into an array structure
524
 
   *
525
 
   * @param string vCard block to parse
526
 
   * @return array Raw data structure
527
 
   */
528
 
  private static function vcard_decode($vcard)
529
 
  {
530
 
    // Perform RFC2425 line unfolding and split lines
531
 
    $vcard = preg_replace(array("/\r/", "/\n\s+/"), '', $vcard);
532
 
    $lines = explode("\n", $vcard);
533
 
    $data  = array();
534
 
 
535
 
    for ($i=0; $i < count($lines); $i++) {
536
 
      if (!preg_match('/^([^:]+):(.+)$/', $lines[$i], $line))
537
 
        continue;
538
 
 
539
 
      if (preg_match('/^(BEGIN|END)$/i', $line[1]))
540
 
        continue;
541
 
 
542
 
      // convert 2.1-style "EMAIL;internet;home:" to 3.0-style "EMAIL;TYPE=internet;TYPE=home:"
543
 
      if (($data['VERSION'][0] == "2.1") && preg_match('/^([^;]+);([^:]+)/', $line[1], $regs2) && !preg_match('/^TYPE=/i', $regs2[2])) {
544
 
        $line[1] = $regs2[1];
545
 
        foreach (explode(';', $regs2[2]) as $prop)
546
 
          $line[1] .= ';' . (strpos($prop, '=') ? $prop : 'TYPE='.$prop);
547
 
      }
548
 
 
549
 
      if (preg_match_all('/([^\\;]+);?/', $line[1], $regs2)) {
550
 
        $entry = array();
551
 
        $field = strtoupper($regs2[1][0]);
552
 
 
553
 
        foreach($regs2[1] as $attrid => $attr) {
554
 
          if ((list($key, $value) = explode('=', $attr)) && $value) {
555
 
            $value = trim($value);
556
 
            if ($key == 'ENCODING') {
557
 
              // add next line(s) to value string if QP line end detected
558
 
              while ($value == 'QUOTED-PRINTABLE' && preg_match('/=$/', $lines[$i]))
559
 
                  $line[2] .= "\n" . $lines[++$i];
560
 
 
561
 
              $line[2] = self::decode_value($line[2], $value);
562
 
            }
563
 
            else
564
 
              $entry[strtolower($key)] = array_merge((array)$entry[strtolower($key)], (array)self::vcard_unquote($value, ','));
565
 
          }
566
 
          else if ($attrid > 0) {
567
 
            $entry[$key] = true;  // true means attr without =value
568
 
          }
569
 
        }
570
 
 
571
 
        $entry = array_merge($entry, (array)self::vcard_unquote($line[2]));
572
 
        $data[$field][] = $entry;
573
 
      }
574
 
    }
575
 
 
576
 
    unset($data['VERSION']);
577
 
    return $data;
578
 
  }
579
 
 
580
 
 
581
 
  /**
582
 
   * Decode a given string with the encoding rule from ENCODING attributes
583
 
   *
584
 
   * @param string String to decode
585
 
   * @param string Encoding type (quoted-printable and base64 supported)
586
 
   * @return string Decoded 8bit value
587
 
   */
588
 
  private static function decode_value($value, $encoding)
589
 
  {
590
 
    switch (strtolower($encoding)) {
591
 
      case 'quoted-printable':
592
 
        self::$values_decoded = true;
593
 
        return quoted_printable_decode($value);
594
 
 
595
 
      case 'base64':
596
 
        self::$values_decoded = true;
597
 
        return base64_decode($value);
598
 
 
599
 
      default:
600
 
        return $value;
601
 
    }
602
 
  }
603
 
 
604
 
 
605
 
  /**
606
 
   * Encodes an entry for storage in our database (vcard 3.0 format, unfolded)
607
 
   *
608
 
   * @param array Raw data structure to encode
609
 
   * @return string vCard encoded string
610
 
   */
611
 
  static function vcard_encode($data)
612
 
  {
613
 
    foreach((array)$data as $type => $entries) {
614
 
      /* valid N has 5 properties */
615
 
      while ($type == "N" && is_array($entries[0]) && count($entries[0]) < 5)
616
 
        $entries[0][] = "";
617
 
 
618
 
      // make sure FN is not empty (required by RFC2426)
619
 
      if ($type == "FN" && empty($entries))
620
 
        $entries[0] = $data['EMAIL'][0][0];
621
 
 
622
 
      foreach((array)$entries as $entry) {
623
 
        $attr = '';
624
 
        if (is_array($entry)) {
625
 
          $value = array();
626
 
          foreach($entry as $attrname => $attrvalues) {
627
 
            if (is_int($attrname))
628
 
              $value[] = $attrvalues;
629
 
            elseif ($attrvalues === true)
630
 
              $attr .= ";$attrname";    // true means just tag, not tag=value, as in PHOTO;BASE64:...
631
 
            else {
632
 
              foreach((array)$attrvalues as $attrvalue)
633
 
                $attr .= ";$attrname=" . self::vcard_quote($attrvalue, ',');
634
 
            }
635
 
          }
636
 
        }
637
 
        else {
638
 
          $value = $entry;
639
 
        }
640
 
 
641
 
        // skip empty entries
642
 
        if (self::is_empty($value))
643
 
          continue;
644
 
 
645
 
        $vcard .= self::vcard_quote($type) . $attr . ':' . self::vcard_quote($value) . self::$eol;
646
 
      }
647
 
    }
648
 
 
649
 
    return 'BEGIN:VCARD' . self::$eol . 'VERSION:3.0' . self::$eol . $vcard . 'END:VCARD';
650
 
  }
651
 
 
652
 
 
653
 
  /**
654
 
   * Join indexed data array to a vcard quoted string
655
 
   *
656
 
   * @param array Field data
657
 
   * @param string Separator
658
 
   * @return string Joined and quoted string
659
 
   */
660
 
  private static function vcard_quote($s, $sep = ';')
661
 
  {
662
 
    if (is_array($s)) {
663
 
      foreach($s as $part) {
664
 
        $r[] = self::vcard_quote($part, $sep);
665
 
      }
666
 
      return(implode($sep, (array)$r));
667
 
    }
668
 
    else {
669
 
      return strtr($s, array('\\' => '\\\\', "\r" => '', "\n" => '\n', ',' => '\,', ';' => '\;'));
670
 
    }
671
 
  }
672
 
 
673
 
 
674
 
  /**
675
 
   * Split quoted string
676
 
   *
677
 
   * @param string vCard string to split
678
 
   * @param string Separator char/string
679
 
   * @return array List with splitted values
680
 
   */
681
 
  private static function vcard_unquote($s, $sep = ';')
682
 
  {
683
 
    // break string into parts separated by $sep, but leave escaped $sep alone
684
 
    if (count($parts = explode($sep, strtr($s, array("\\$sep" => "\007")))) > 1) {
685
 
      foreach($parts as $s) {
686
 
        $result[] = self::vcard_unquote(strtr($s, array("\007" => "\\$sep")), $sep);
687
 
      }
688
 
      return $result;
689
 
    }
690
 
    else {
691
 
      return strtr($s, array("\r" => '', '\\\\' => '\\', '\n' => "\n", '\N' => "\n", '\,' => ',', '\;' => ';', '\:' => ':'));
692
 
    }
693
 
  }
694
 
 
695
 
 
696
 
  /**
697
 
   * Check if vCard entry is empty: empty string or an array with
698
 
   * all entries empty.
699
 
   *
700
 
   * @param mixed $value Attribute value (string or array)
701
 
   *
702
 
   * @return bool True if the value is empty, False otherwise
703
 
   */
704
 
  private static function is_empty($value)
705
 
  {
706
 
    foreach ((array)$value as $v) {
707
 
      if (((string)$v) !== '') {
708
 
        return false;
709
 
      }
710
 
    }
711
 
 
712
 
    return true;
713
 
  }
714
 
 
715
 
  /**
716
 
   * Extract array values by a filter
717
 
   *
718
 
   * @param array Array to filter
719
 
   * @param keys Array or comma separated list of values to keep
720
 
   * @param boolean Invert key selection: remove the listed values
721
 
   * @return array The filtered array
722
 
   */
723
 
  private static function array_filter($arr, $values, $inverse = false)
724
 
  {
725
 
    if (!is_array($values))
726
 
      $values = explode(',', $values);
727
 
 
728
 
    $result = array();
729
 
    $keep = array_flip((array)$values);
730
 
    foreach ($arr as $key => $val)
731
 
      if ($inverse != isset($keep[strtolower($val)]))
732
 
        $result[$key] = $val;
733
 
 
734
 
    return $result;
735
 
  }
736
 
 
737
 
  /**
738
 
   * Returns UNICODE type based on BOM (Byte Order Mark)
739
 
   *
740
 
   * @param string Input string to test
741
 
   * @return string Detected encoding
742
 
   */
743
 
  private static function detect_encoding($string)
744
 
  {
745
 
    if (substr($string, 0, 4) == "\0\0\xFE\xFF") return 'UTF-32BE';  // Big Endian
746
 
    if (substr($string, 0, 4) == "\xFF\xFE\0\0") return 'UTF-32LE';  // Little Endian
747
 
    if (substr($string, 0, 2) == "\xFE\xFF")     return 'UTF-16BE';  // Big Endian
748
 
    if (substr($string, 0, 2) == "\xFF\xFE")     return 'UTF-16LE';  // Little Endian
749
 
    if (substr($string, 0, 3) == "\xEF\xBB\xBF") return 'UTF-8';
750
 
 
751
 
    // heuristics
752
 
    if ($string[0] == "\0" && $string[1] == "\0" && $string[2] == "\0" && $string[3] != "\0") return 'UTF-32BE';
753
 
    if ($string[0] != "\0" && $string[1] == "\0" && $string[2] == "\0" && $string[3] == "\0") return 'UTF-32LE';
754
 
    if ($string[0] == "\0" && $string[1] != "\0" && $string[2] == "\0" && $string[3] != "\0") return 'UTF-16BE';
755
 
    if ($string[0] != "\0" && $string[1] == "\0" && $string[2] != "\0" && $string[3] == "\0") return 'UTF-16LE';
756
 
 
757
 
    // use mb_detect_encoding()
758
 
    $encodings = array('UTF-8', 'ISO-8859-1', 'ISO-8859-2', 'ISO-8859-3',
759
 
      'ISO-8859-4', 'ISO-8859-5', 'ISO-8859-6', 'ISO-8859-7', 'ISO-8859-8', 'ISO-8859-9',
760
 
      'ISO-8859-10', 'ISO-8859-13', 'ISO-8859-14', 'ISO-8859-15', 'ISO-8859-16',
761
 
      'WINDOWS-1252', 'WINDOWS-1251', 'BIG5', 'GB2312');
762
 
 
763
 
    if (function_exists('mb_detect_encoding') && ($enc = mb_detect_encoding($string, $encodings)))
764
 
      return $enc;
765
 
 
766
 
    // No match, check for UTF-8
767
 
    // from http://w3.org/International/questions/qa-forms-utf-8.html
768
 
    if (preg_match('/\A(
769
 
        [\x09\x0A\x0D\x20-\x7E]
770
 
        | [\xC2-\xDF][\x80-\xBF]
771
 
        | \xE0[\xA0-\xBF][\x80-\xBF]
772
 
        | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}
773
 
        | \xED[\x80-\x9F][\x80-\xBF]
774
 
        | \xF0[\x90-\xBF][\x80-\xBF]{2}
775
 
        | [\xF1-\xF3][\x80-\xBF]{3}
776
 
        | \xF4[\x80-\x8F][\x80-\xBF]{2}
777
 
        )*\z/xs', substr($string, 0, 2048)))
778
 
      return 'UTF-8';
779
 
 
780
 
    return rcmail::get_instance()->config->get('default_charset', 'ISO-8859-1'); # fallback to Latin-1
781
 
  }
782
 
 
783
 
}