Passed
Branch 2.0.0-dev (9fc37f)
by Jeroen
02:57
created

VcfParser::parseVCard()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace JeroenDesloovere\VCard\Parser;
6
7
use JeroenDesloovere\VCard\Exception\ParserException;
8
use JeroenDesloovere\VCard\Parser\Property\NodeParserInterface;
9
use JeroenDesloovere\VCard\Property\NodeInterface;
10
use JeroenDesloovere\VCard\VCard;
11
12
final class VcfParser implements ParserInterface
13
{
14
    /** @var NodeParserInterface[] - f.e. ['ADR' => JeroenDesloovere\VCard\Parser\Property\AddressParser] */
15
    private $parsers = [];
16
17
    public function __construct()
18
    {
19
        /**
20
         * We define all possible node parsers
21
         *
22
         * @var NodeInterface $node
23
         */
24
        foreach (VCard::POSSIBLE_VALUES as $node) {
25
            $this->parsers[$node::getNode()] = $node::getParser();
26
        }
27
    }
28
29
    /**
30
     * Returns all found vCard objects
31
     *
32
     * @param string $content
33
     * @return VCard[]
34
     * @throws ParserException
35
     */
36
    public function getVCards(string $content): array
37
    {
38
        return array_map(function ($vCardContent) {
39
            return $this->parseVCard($vCardContent);
40
        }, $this->splitIntoVCards($content));
41
    }
42
43
    private function parseParameters(?string $parameters): array
44
    {
45
        if ($parameters === null) {
46
            return [];
47
        }
48
49
        /** @var string[] $parametersArray */
50
        $parametersArray = explode(';', $parameters);
51
        $parsedParameters = [];
52
        foreach ($parametersArray as $parameter) {
53
            /**
54
             * @var string $node
55
             * @var string $value
56
             */
57
            @list($node, $value) = explode('=', $parameter, 2);
58
59
            if (array_key_exists($node, $this->parsers)) {
60
                $parsedParameters[$node] = $this->parsers[$node]->parseLine($value);
61
            }
62
        }
63
64
        return $parsedParameters;
65
    }
66
67
    private function parseVCard(string $content): VCard
68
    {
69
        $vCard = new VCard();
70
        $lines = explode("\n", $content);
71
        foreach ($lines as $line) {
72
            $this->parseVCardLine($line, $vCard);
73
        }
74
75
        return $vCard;
76
    }
77
78
    private function parseVCardLine(string $line, VCard &$vCard): void
79
    {
80
        // Strip grouping information. We don't use the group names. We
81
        // simply use a list for entries that have multiple values.
82
        // As per RFC, group names are alphanumerical, and end with a
83
        // period (.).
84
        $line = preg_replace('/^\w+\./', '', trim($line));
85
86
        /**
87
         * @var string $node
88
         * @var string $value
89
         */
90
        @list($node, $value) = explode(':', $line, 2);
91
92
        /**
93
         * @var string $node
94
         * @var string|null $parameterContent
95
         */
96
        @list($node, $parameterContent) = explode(';', $node, 2);
97
98
        // Skip parameters that we can not parse yet, because the property/parser does not exist yet.
99
        // Feel free to create a PR to add a new Property Parser
100
        if (!array_key_exists($node, $this->parsers)) {
101
            return;
102
        }
103
104
        try {
105
            $vCard->add($this->parsers[$node]->parseLine($value, $this->parseParameters($parameterContent)));
106
        } catch (\Exception $e) {
107
            // Ignoring properties that throw error. F.e. if they are allowed only once.
108
        }
109
    }
110
111
    /**
112
     * Split string into array, each array item contains vCard content.
113
     *
114
     * @param string $content - The full content from the .vcf file.
115
     * @return array - Is an array with the content for all possible vCards.
116
     * @throws ParserException
117
     */
118
    private function splitIntoVCards(string $content): array
119
    {
120
        // Normalize new lines.
121
        $content = trim(str_replace(["\r\n", "\r"], "\n", $content));
122
123
        if (!preg_match('/^BEGIN:VCARD[\s\S]+END:VCARD$/', $content)) {
124
            throw ParserException::forUnreadableVCard($content);
125
        }
126
127
        // Remove first BEGIN:VCARD and last END:VCARD
128
        $content = substr($content, 12, -10);
129
130
        // RFC2425 5.8.1. Line delimiting and folding
131
        // Unfolding is accomplished by regarding CRLF immediately followed by
132
        // a white space character (namely HTAB ASCII decimal 9 or. SPACE ASCII
133
        // decimal 32) as equivalent to no characters at all (i.e., the CRLF
134
        // and single white space character are removed).
135
        $content = preg_replace("/\n(?:[ \t])/", '', $content);
136
137
        // If multiple vcards split per vcard
138
        return preg_split('/\nEND:VCARD\s+BEGIN:VCARD\n/', $content);
0 ignored issues
show
Bug Best Practice introduced by
The expression return preg_split('/\nEN...IN:VCARD\n/', $content) could return the type false which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
139
    }
140
}
141