Passed
Push — develop ( 535216...2a3c68 )
by Paul
07:18
created

Email::app()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 1
1
<?php
2
3
namespace GeminiLabs\SiteReviews\Modules;
4
5
use GeminiLabs\SiteReviews\Arguments;
6
use GeminiLabs\SiteReviews\Contracts\EmailContract;
7
use GeminiLabs\SiteReviews\Contracts\PluginContract;
8
use GeminiLabs\SiteReviews\Contracts\TemplateContract;
9
use GeminiLabs\SiteReviews\Database\OptionManager;
10
use GeminiLabs\SiteReviews\Defaults\DefaultsAbstract;
11
use GeminiLabs\SiteReviews\Defaults\EmailDefaults;
12
use GeminiLabs\SiteReviews\Helpers\Str;
13
use GeminiLabs\SiteReviews\Modules\Html\Template;
14
15
class Email implements EmailContract
16
{
17
    /** @var array */
18
    public $attachments;
19
20
    /** @var array */
21
    public $data;
22
23
    /** @var array */
24
    public $email;
25
26
    /** @var array */
27
    public $headers;
28
29
    /** @var string */
30
    public $message;
31
32
    /** @var string */
33
    public $subject;
34
35
    /** @var string|array */
36
    public $to;
37
38 1
    public function app(): PluginContract
39
    {
40 1
        return glsr();
41
    }
42
43 1
    public function compose(array $email, array $data = []): EmailContract
44
    {
45 1
        $this->data = $data;
46 1
        $this->normalize($email);
47 1
        $this->attachments = $this->email['attachments'];
48 1
        $this->headers = $this->buildHeaders();
49 1
        $this->message = $this->buildHtmlMessage();
50 1
        $this->subject = $this->email['subject'];
51 1
        $this->to = $this->email['to'];
52 1
        add_action('phpmailer_init', [$this, 'buildPlainTextMessage']);
53 1
        return $this;
54
    }
55
56
    public function data(): Arguments
57
    {
58
        return glsr()->args($this->data);
59
    }
60
61 1
    public function defaults(): DefaultsAbstract
62
    {
63 1
        return glsr(EmailDefaults::class);
1 ignored issue
show
Bug Best Practice introduced by
The expression return glsr(GeminiLabs\S...s\EmailDefaults::class) could return the type callable which is incompatible with the type-hinted return GeminiLabs\SiteReviews\Defaults\DefaultsAbstract. Consider adding an additional type-check to rule them out.
Loading history...
64
    }
65
66
    public function logMailError(\WP_Error $error): void
67
    {
68
        glsr_log()
69
            ->error('[wp_mail] Email was not sent: '. $error->get_error_message())
70
            ->debug(['Email' => $this, 'WP_Error' => $error]);
71
    }
72
73
    public function read(string $format = ''): string
74
    {
75
        if ('plaintext' === $format) {
76
            $message = $this->stripHtmlTags($this->message);
77
            return $this->app()->filterString('email/message', $message, 'text', $this);
78
        }
79
        return $this->message;
80
    }
81
82 1
    public function send(): bool
83
    {
84 1
        $required = [
85 1
            'message' => !empty($this->message),
86 1
            'recipient' => !empty($this->to),
87 1
            'subject' => !empty($this->subject),
88 1
        ];
89 1
        $missing = array_keys(array_diff($required, array_filter($required)));
90 1
        if (!empty($missing)) {
91
            glsr_log()->warning(sprintf('The email is missing the %s', Str::naturalJoin($missing)));
92
            return false;
93
        }
94 1
        add_action('wp_mail_failed', [$this, 'logMailError']);
95 1
        $sent = wp_mail(
96 1
            $this->to,
97 1
            $this->subject,
98 1
            $this->message,
99 1
            $this->headers,
100 1
            $this->attachments
101 1
        );
102 1
        remove_action('wp_mail_failed', [$this, 'logMailError']);
103 1
        $this->reset();
104 1
        return $sent;
105
    }
106
107 1
    public function template(): TemplateContract
108
    {
109 1
        return glsr(Template::class);
1 ignored issue
show
Bug Best Practice introduced by
The expression return glsr(GeminiLabs\S...s\Html\Template::class) could return the type callable which is incompatible with the type-hinted return GeminiLabs\SiteReviews\Contracts\TemplateContract. Consider adding an additional type-check to rule them out.
Loading history...
110
    }
111
112
    /**
113
     * @action phpmailer_init
114
     */
115 1
    public function buildPlainTextMessage($phpmailer): void
116
    {
117 1
        if (empty($this->email)) {
118
            return;
119
        }
120 1
        if ('text/plain' === $phpmailer->ContentType || !empty($phpmailer->AltBody)) {
121
            return;
122
        }
123 1
        $message = $this->stripHtmlTags($phpmailer->Body);
124 1
        $phpmailer->AltBody = $this->app()->filterString('email/message', $message, 'text', $this);
125
    }
126
127 1
    protected function buildHeaders(): array
128
    {
129 1
        $allowed = [
130 1
            'bcc', 'cc', 'from', 'reply-to',
131 1
        ];
132 1
        $headers = array_intersect_key($this->email, array_flip($allowed));
133 1
        $headers = array_filter($headers);
134 1
        foreach ($headers as $key => $value) {
135 1
            unset($headers[$key]);
136 1
            $headers[] = $key.': '.$value;
137
        }
138 1
        $headers[] = 'Content-Type: text/html';
139 1
        return $this->app()->filterArray('email/headers', $headers, $this);
140
    }
141
142 1
    protected function buildHtmlMessage(): string
143
    {
144 1
        $message = $this->buildMessage();
145 1
        $message = $this->email['before'].$message.$this->email['after'];
146 1
        $message = strip_shortcodes($message);
147 1
        $message = wptexturize($message);
148 1
        $message = wpautop($message);
149 1
        $message = str_replace('&lt;&gt; ', '', $message);
150 1
        $message = str_replace(']]>', ']]&gt;', $message);
151 1
        $context = wp_parse_args(['message' => $message], $this->email['template-tags']);
152 1
        $message = $this->template()->build('templates/emails/'.$this->email['template'], [
153 1
            'context' => $context,
154 1
        ]);
155 1
        return $this->app()->filterString('email/message', stripslashes($message), 'html', $this);
156
    }
157
158 1
    protected function buildMessage(): string
159
    {
160 1
        if (!empty($this->email['message'])) {
161
            return $this->email['message'];
162
        }
163 1
        $template = trim(glsr(OptionManager::class)->get('settings.general.notification_message'));
164 1
        if (!empty($template)) {
165 1
            $context = ['context' => $this->email['template-tags']];
166 1
            $templatePathForHook = 'notification_message';
167 1
            return $this->template()->interpolate($template, $templatePathForHook, $context);
168
        }
169
        return '';
170
    }
171
172 1
    protected function normalize(array $email = []): void
173
    {
174 1
        $email = $this->defaults()->restrict($email);
175 1
        $this->email = $this->app()->filterArray('email/compose', $email, $this);
176
    }
177
178 1
    protected function reset(): void
179
    {
180 1
        $this->attachments = [];
181 1
        $this->data = [];
182 1
        $this->email = [];
183 1
        $this->headers = [];
184 1
        $this->message = '';
185 1
        $this->subject = '';
186 1
        $this->to = '';
187
    }
188
189 1
    protected function stripHtmlTags($string): string
190
    {
191
        // remove invisible elements
192 1
        $string = preg_replace('@<(embed|head|noembed|noscript|object|script|style)[^>]*?>.*?</\\1>@siu', '', $string);
193
        // replace link elements
194 1
        $string = preg_replace_callback('@<a[^>]*href=("|\')(.*?)\1[^>]*>(.*?)<\/a>@iu', function ($matches) {
195 1
            $matches = array_map('trim', $matches);
196 1
            return ($matches[2] !== $matches[3])
197 1
                ? sprintf('%s (%s)', $matches[3], $matches[2])
198 1
                : $matches[2];
199 1
        }, $string);
200
        // replace certain elements with a line-break
201 1
        $string = preg_replace('@</(div|h[1-9]|p|pre|tr)@iu', "\r\n\$0", $string);
202
        // replace other elements with a space
203 1
        $string = preg_replace('@</(td|th)@iu', ' $0', $string);
204
        // add a placeholder for plain-text bullets to list elements
205 1
        $string = preg_replace('@<(li)[^>]*?>@siu', '$0-o-^-o-', $string);
206
        // strip all remaining HTML tags
207 1
        $string = wp_strip_all_tags($string);
208 1
        $string = wp_specialchars_decode($string, ENT_QUOTES);
209 1
        $string = preg_replace('/\v(?:[\v\h]+){2,}/u', "\r\n\r\n", $string);
210 1
        $string = str_replace('-o-^-o-', ' - ', $string);
211 1
        return html_entity_decode($string, ENT_QUOTES, 'UTF-8');
212
    }
213
}
214