Completed
Push — master ( b352fa...197388 )
by Jeroen
02:13
created

VCardParser   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 314
Duplicated Lines 6.69 %

Coupling/Cohesion

Components 1
Dependencies 0

Importance

Changes 0
Metric Value
wmc 55
c 0
b 0
f 0
lcom 1
cbo 0
dl 21
loc 314
rs 6.8

14 Methods

Rating   Name   Duplication   Size   Complexity  
A parseFromFile() 0 8 3
A __construct() 0 7 1
A rewind() 0 4 1
A current() 0 6 2
A key() 0 4 1
A next() 0 4 1
A valid() 0 4 1
A getCards() 0 4 1
A getCardAtIndex() 0 7 2
F parse() 21 147 38
A parseName() 0 17 1
A parseBirthday() 0 4 1
A parseAddress() 0 21 1
A unescape() 0 4 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 VCardParser 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 VCardParser, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace JeroenDesloovere\VCard;
4
5
/*
6
 * Copyright 2010 Thomas Schaaf <[email protected]>
7
 *
8
 * Licensed under the Apache License, Version 2.0 (the "License");
9
 * you may not use this file except in compliance with the License.
10
 * You may obtain a copy of the License at
11
 *
12
 *     http://www.apache.org/licenses/LICENSE-2.0
13
 *
14
 * Unless required by applicable law or agreed to in writing, software
15
 * distributed under the License is distributed on an "AS IS" BASIS,
16
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
 * See the License for the specific language governing permissions and
18
 * limitations under the License.
19
 *
20
 * Changes by: Wouter Admiraal <[email protected]>
21
 * Original code is available at: http://code.google.com/p/zendvcard/
22
 */
23
24
use Iterator;
25
26
/**
27
 * VCard PHP Class to parse .vcard files.
28
 *
29
 * This class is heavily based on the Zendvcard project (seemingly abandoned),
30
 * which is licensed under the Apache 2.0 license.
31
 * More information can be found at https://code.google.com/archive/p/zendvcard/
32
 *
33
 * @author Thomas Schaaf <[email protected]>
34
 * @author ruzicka.jan
35
 * @author Wouter Admiraal <[email protected]>
36
 */
37
class VCardParser implements Iterator
38
{
39
    /**
40
     * The raw VCard content.
41
    *
42
     * @var string
43
     */
44
    protected $content;
45
46
    /**
47
     * The VCard data objects.
48
     *
49
     * @var array
50
     */
51
    protected $vcardObjects;
52
53
    /**
54
     * The iterator position.
55
     *
56
     * @var int
57
     */
58
    protected $position;
59
60
    /**
61
     * Helper function to parse a file directly.
62
     *
63
     * @param string $filename
64
     *
65
     * @return JeroenDesloovere\VCard\VCardParser
66
     */
67
    public static function parseFromFile($filename)
68
    {
69
        if (file_exists($filename) && is_readable($filename)) {
70
            return new VCardParser(file_get_contents($filename));
71
        } else {
72
            throw new \RuntimeException(sprintf("File %s is not readable, or doesn't exist.", $filename));
73
        }
74
    }
75
76
    public function __construct($content)
77
    {
78
        $this->content = $content;
79
        $this->vcardObjects = array();
80
        $this->rewind();
81
        $this->parse();
82
    }
83
84
    public function rewind()
85
    {
86
        $this->position = 0;
87
    }
88
89
    public function current()
90
    {
91
        if ($this->valid()) {
92
            return $this->getCardAtIndex($this->position);
93
        }
94
    }
95
96
    public function key()
97
    {
98
        return $this->position;
99
    }
100
101
    public function next()
102
    {
103
        $this->position++;
104
    }
105
106
    public function valid()
107
    {
108
        return !empty($this->vcardObjects[$this->position]);
109
    }
110
111
    /**
112
     * Fetch all the imported VCards.
113
     *
114
     * @return array
115
     *    A list of VCard card data objects.
116
     */
117
    public function getCards()
118
    {
119
        return $this->vcardObjects;
120
    }
121
122
    /**
123
     * Fetch the imported VCard at the specified index.
124
     *
125
     * @throws OutOfBoundsException
126
     *
127
     * @param int $i
128
     *
129
     * @return stdClass
130
     *    The card data object.
131
     */
132
    public function getCardAtIndex($i)
133
    {
134
        if (isset($this->vcardObjects[$i])) {
135
            return $this->vcardObjects[$i];
136
        }
137
        throw new \OutOfBoundsException();
138
    }
139
140
    /**
141
     * Start the parsing process.
142
     *
143
     * This method will populate the data object.
144
     */
145
    protected function parse()
146
    {
147
        // Normalize new lines.
148
        $this->content = str_replace(array("\r\n", "\r"), "\n", $this->content);
149
150
        // RFC2425 5.8.1. Line delimiting and folding
151
        // Unfolding is accomplished by regarding CRLF immediately followed by
152
        // a white space character (namely HTAB ASCII decimal 9 or. SPACE ASCII
153
        // decimal 32) as equivalent to no characters at all (i.e., the CRLF
154
        // and single white space character are removed).
155
        $this->content = preg_replace("/\n(?:[ \t])/", "", $this->content);
156
        $lines = explode("\n", $this->content);
157
158
        // Parse the VCard, line by line.
159
        foreach ($lines as $line) {
160
            $line = trim($line);
161
162
            if (strtoupper($line) == "BEGIN:VCARD") {
163
                $cardData = new \stdClass();
164
            } elseif (strtoupper($line) == "END:VCARD") {
165
                $this->vcardObjects[] = $cardData;
0 ignored issues
show
Bug introduced by
The variable $cardData does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
166
            } elseif (!empty($line)) {
167
                // Strip grouping information. We don't use the group names. We
168
                // simply use a list for entries that have multiple values.
169
                // As per RFC, group names are alphanumerical, and end with a
170
                // period (.).
171
                $line = preg_replace('/^\w+\./', '', $line);
172
173
                $type = '';
174
                $value = '';
175
                @list($type, $value) = explode(':', $line, 2);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
176
177
                $types = explode(';', $type);
178
                $element = strtoupper($types[0]);
179
180
                array_shift($types);
181
182
                // Normalize types. A type can either be a type-param directly,
183
                // or can be prefixed with "type=". E.g.: "INTERNET" or
184
                // "type=INTERNET".
185
                if (!empty($types)) {
186
                    $types = array_map(function($type) {
187
                        return preg_replace('/^type=/i', '', $type);
188
                    }, $types);
189
                }
190
191
                $i = 0;
192
                $rawValue = false;
193
                foreach ($types as $type) {
194
                    if (preg_match('/base64/', strtolower($type))) {
195
                        $value = base64_decode($value);
196
                        unset($types[$i]);
197
                        $rawValue = true;
198
                    } elseif (preg_match('/encoding=b/', strtolower($type))) {
199
                        $value = base64_decode($value);
200
                        unset($types[$i]);
201
                        $rawValue = true;
202
                    } elseif (preg_match('/quoted-printable/', strtolower($type))) {
203
                        $value = quoted_printable_decode($value);
204
                        unset($types[$i]);
205
                        $rawValue = true;
206
                    } elseif (strpos(strtolower($type), 'charset=') === 0) {
207
                        try {
208
                            $value = mb_convert_encoding($value, "UTF-8", substr($type, 8));
209
                        } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
210
                        }
211
                        unset($types[$i]);
212
                    }
213
                    $i++;
214
                }
215
216
                switch (strtoupper($element)) {
217
                    case 'FN':
218
                        $cardData->fullname = $value;
219
                        break;
220
                    case 'N':
221
                        foreach ($this->parseName($value) as $key => $val) {
222
                            $cardData->{$key} = $val;
223
                        }
224
                        break;
225
                    case 'BDAY':
226
                        $cardData->birthday = $this->parseBirthday($value);
227
                        break;
228
                    case 'ADR':
229
                        if (!isset($cardData->address)) {
230
                            $cardData->address = array();
231
                        }
232
                        $key = !empty($types) ? implode(';', $types) : 'WORK;POSTAL';
233
                        $cardData->address[$key][] = $this->parseAddress($value);
234
                        break;
235 View Code Duplication
                    case 'TEL':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
236
                        if (!isset($cardData->phone)) {
237
                            $cardData->phone = array();
238
                        }
239
                        $key = !empty($types) ? implode(';', $types) : 'default';
240
                        $cardData->phone[$key][] = $value;
241
                        break;
242 View Code Duplication
                    case 'EMAIL':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
243
                        if (!isset($cardData->email)) {
244
                            $cardData->email = array();
245
                        }
246
                        $key = !empty($types) ? implode(';', $types) : 'default';
247
                        $cardData->email[$key][] = $value;
248
                        break;
249
                    case 'REV':
250
                        $cardData->revision = $value;
251
                        break;
252
                    case 'VERSION':
253
                        $cardData->version = $value;
254
                        break;
255
                    case 'ORG':
256
                        $cardData->organization = $value;
257
                        break;
258 View Code Duplication
                    case 'URL':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
259
                        if (!isset($cardData->url)) {
260
                            $cardData->url = array();
261
                        }
262
                        $key = !empty($types) ? implode(';', $types) : 'default';
263
                        $cardData->url[$key][] = $value;
264
                        break;
265
                    case 'TITLE':
266
                        $cardData->title = $value;
267
                        break;
268
                    case 'PHOTO':
269
                        if ($rawValue) {
270
                            $cardData->rawPhoto = $value;
271
                        } else {
272
                            $cardData->photo = $value;
273
                        }
274
                        break;
275
                    case 'LOGO':
276
                        if ($rawValue) {
277
                            $cardData->rawLogo = $value;
278
                        } else {
279
                            $cardData->logo = $value;
280
                        }
281
                        break;
282
                    case 'NOTE':
283
                        $cardData->note = $this->unescape($value);
284
                        break;
285
                    case 'CATEGORIES':
286
                        $cardData->categories = array_map('trim', explode(',', $value));
287
                        break;
288
                }
289
            }
290
        }
291
    }
292
293
    protected function parseName($value)
294
    {
295
        @list(
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
296
            $lastname,
297
            $firstname,
298
            $additional,
299
            $prefix,
300
            $suffix
301
        ) = explode(';', $value);
302
        return (object) array(
303
            'lastname' => $lastname,
304
            'firstname' => $firstname,
305
            'additional' => $additional,
306
            'prefix' => $prefix,
307
            'suffix' => $suffix,
308
        );
309
    }
310
311
    protected function parseBirthday($value)
312
    {
313
        return new \DateTime($value);
314
    }
315
316
    protected function parseAddress($value)
317
    {
318
        @list(
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
319
            $name,
320
            $extended,
321
            $street,
322
            $city,
323
            $region,
324
            $zip,
325
            $country,
326
        ) = explode(';', $value);
327
        return (object) array(
328
            'name' => $name,
329
            'extended' => $extended,
330
            'street' => $street,
331
            'city' => $city,
332
            'region' => $region,
333
            'zip' => $zip,
334
            'country' => $country,
335
        );
336
    }
337
338
    /**
339
     * Unescape newline characters according to RFC2425 section 5.8.4.
340
     * This function will replace escaped line breaks with PHP_EOL.
341
     *
342
     * @link http://tools.ietf.org/html/rfc2425#section-5.8.4
343
     * @param  string $text
344
     * @return string
345
     */
346
    protected function unescape($text)
347
    {
348
        return str_replace("\\n", PHP_EOL, $text);
349
    }
350
}
351