Completed
Pull Request — master (#84)
by Frederik
02:30
created

FixedQuotation   A

Complexity

Total Complexity 25

Size/Duplication

Total Lines 214
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 86.92%

Importance

Changes 0
Metric Value
wmc 25
lcom 1
cbo 4
dl 0
loc 214
ccs 93
cts 107
cp 0.8692
rs 10
c 0
b 0
f 0

5 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
B quote() 0 47 6
C quoteHtml() 0 72 11
B prepareBody() 0 39 6
A quoteText() 0 14 1
1
<?php
2
declare(strict_types=1);
3
4
namespace Genkgo\Mail\Quotation;
5
6
use Genkgo\Mail\Address;
7
use Genkgo\Mail\AlternativeText;
8
use Genkgo\Mail\MessageBodyCollection;
9
use Genkgo\Mail\MessageInterface;
10
use Genkgo\Mail\QuotationInterface;
11
12
final class FixedQuotation implements QuotationInterface
13
{
14
    /**
15
     * @var string
16
     */
17
    private $headerText;
18
19
    /**
20
     * @param string $headerText
21
     */
22 13
    public function __construct(string $headerText = '%s (%s):')
23
    {
24 13
        $this->headerText = $headerText;
25 13
    }
26
27
    /**
28
     * @param MessageBodyCollection $body
29
     * @param MessageInterface $originalMessage
30
     * @return MessageBodyCollection
31
     * @throws \DOMException
32
     */
33 13
    public function quote(MessageBodyCollection $body, MessageInterface $originalMessage): MessageBodyCollection
34
    {
35 13
        $originalBody = MessageBodyCollection::extract($originalMessage);
36 13
        $dateString = 'unknown';
37 13
        foreach ($originalMessage->getHeader('Date') as $header) {
38
            try {
39 6
                $date = new \DateTimeImmutable($header->getValue()->getRaw());
40 6
                $formatter = \IntlDateFormatter::create(
41 6
                    \Locale::getDefault(),
42 6
                    \IntlDateFormatter::MEDIUM,
43 6
                    \IntlDateFormatter::MEDIUM
44
                );
45
46 6
                if ($formatter === false) {
47
                    throw new \UnexpectedValueException('Cannot create date formatter');
48
                }
49
50 6
                $dateString = $formatter->format($date);
51 6
                break;
52
            } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
53
            }
54
        }
55
56 13
        $fromString = '';
57 13
        foreach ($originalMessage->getHeader('From') as $header) {
58 13
            $from = Address::fromString($header->getValue()->getRaw());
59 13
            $fromString = $from->getName() ? $from->getName() : (string)$from->getAddress();
60
        }
61
62 13
        $headerText = \sprintf($this->headerText, $fromString, $dateString);
63
64
        return $body
65 13
            ->withHtmlAndNoGeneratedAlternativeText(
66 13
                $this->quoteHtml(
67 13
                    $body->getHtml(),
68 13
                    $originalBody->getHtml(),
69
                    $headerText
70
                )
71
            )
72 13
            ->withAlternativeText(
73 13
                $this->quoteText(
74 13
                    $body->getText(),
75 13
                    $originalBody->getText(),
76
                    $headerText
77
                )
78
            );
79
    }
80
81
    /**
82
     * @param string $newHtml
83
     * @param string $originalHtml
84
     * @param string $headerText
85
     * @return string
86
     * @throws \DOMException
87
     */
88 13
    private function quoteHtml(string $newHtml, string $originalHtml, string $headerText): string
89
    {
90 13
        $originalHtml = \trim($originalHtml);
91 13
        if ($originalHtml === '') {
92 2
            return '';
93
        }
94
95 11
        $document = new \DOMDocument();
96 11
        $document->substituteEntities = false;
97 11
        $document->resolveExternals = false;
98
99 11
        $result = @$document->loadHTML($originalHtml);
100 11
        if ($result === false) {
101
            throw new \DOMException('Incorrect HTML');
102
        }
103
104 11
        if ($document->documentElement !== null) {
105 11
            $query = new \DOMXPath($document);
106 11
            $removeItems = $query->query('//head|//script|//body/@style|//html/@style', $document->documentElement);
107 11
            if ($removeItems instanceof \DOMNodeList) {
108
                /** @var \DOMElement $removeItem */
109 11
                foreach ($removeItems as $removeItem) {
110
                    try {
111
                        /** @var \DOMElement $parent */
112 2
                        $parent = $removeItem->parentNode;
113 2
                        $parent->removeChild($removeItem);
114
                    } catch (\DOMException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
115
                    }
116
                }
117
            }
118
119 11
            $body = $document->getElementsByTagName('body');
120 11
            $quote = $document->createElement('blockquote');
121 11
            $quote->setAttribute('type', 'cite');
122
123 11
            if ($body->length === 0) {
124
                $quote->appendChild($document->removeChild($document->documentElement));
125
            } else {
126 11
                $root = $body->item(0);
127 11
                if ($root instanceof \DOMElement) {
128 11
                    while ($root->childNodes->length !== 0) {
129
                        /** @var \DOMElement $child */
130 11
                        $child = $root->childNodes->item(0);
131 11
                        $quote->appendChild($child);
132
                    }
133
                }
134
            }
135
136 11
            $newDocument = new \DOMDocument();
137 11
            $newDocument->substituteEntities = false;
138 11
            $newDocument->resolveExternals = false;
139 11
            $result = @$newDocument->loadHTML($newHtml, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
140 11
            if ($result === false) {
141
                throw new \DOMException('Incorrect HTML');
142
            }
143
144 11
            $quotedNode = $newDocument->importNode($quote, true);
145 11
            $newBody = $this->prepareBody($newDocument);
146 11
            $newBody->appendChild($quotedNode);
147
148 11
            $header = $newDocument->createElement('p');
149 11
            $header->textContent = $headerText;
150
151
            /** @var \DOMElement $parent */
152 11
            $parent = $quotedNode->parentNode;
153 11
            $parent->insertBefore($header, $quotedNode);
154
155 11
            return \trim((string)$newDocument->saveHTML());
156
        }
157
158
        return '';
159
    }
160
161
    /**
162
     * @param \DOMDocument $document
163
     * @return \DOMElement
164
     */
165 11
    private function prepareBody(\DOMDocument $document): \DOMElement
166
    {
167 11
        if (!$document->documentElement) {
168
            throw new \UnexpectedValueException('Cannot prepare empty document');
169
        }
170
171 11
        $bodyList = $document->getElementsByTagName('body');
172 11
        if ($bodyList->length === 0) {
173 2
            $html = $document->createElement('html');
174 2
            $body = $document->createElement('body');
175 2
            $html->appendChild($body);
176 2
            $body->appendChild($document->documentElement);
177 2
            if ($document->documentElement instanceof \DOMElement) {
178
                $document->removeChild($document->documentElement);
179
            }
180 2
            $document->appendChild($html);
181 2
            return $body;
182
        }
183
184
        /** @var \DOMElement $body */
185 9
        $body = $bodyList->item(0);
186
187 9
        $queryHtml = new \DOMXPath($document);
188 9
        $htmlTags = $queryHtml->query('//html');
189 9
        if ($htmlTags && $htmlTags->length > 0) {
190
            /** @var \DOMElement $html */
191 9
            $html = $htmlTags->item(0);
192 9
            $html->appendChild($body);
193 9
            $document->removeChild($document->documentElement);
194 9
            $document->appendChild($html);
195 9
            return $body;
196
        }
197
198
        $html = $document->createElement('html');
199
        $html->appendChild($body);
200
        $document->removeChild($document->documentElement);
201
        $document->appendChild($html);
202
        return $body;
203
    }
204
205
    /**
206
     * @param AlternativeText $newText
207
     * @param AlternativeText $originalText
208
     * @param string $headerText
209
     * @return AlternativeText
210
     */
211 13
    private function quoteText(
212
        AlternativeText $newText,
213
        AlternativeText $originalText,
214
        string $headerText
215
    ): AlternativeText {
216 13
        return new AlternativeText(
217 13
            \sprintf(
218 13
                "%s\n\n%s\n>%s",
219 13
                (string)$newText,
220 13
                $headerText,
221 13
                \str_replace("\n", "\n>", $originalText->getRaw())
222
            )
223
        );
224
    }
225
}
226