Passed
Push — master ( 05ae42...4e09ff )
by Stefan
07:31
created

OutsideComm::sendSMS()   C

Complexity

Conditions 7
Paths 8

Size

Total Lines 43
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 43
c 0
b 0
f 0
rs 6.7272
cc 7
eloc 30
nc 8
nop 2
1
<?php
2
3
/*
4
 * ******************************************************************************
5
 * Copyright 2011-2017 DANTE Ltd. and GÉANT on behalf of the GN3, GN3+, GN4-1 
6
 * and GN4-2 consortia
7
 *
8
 * License: see the web/copyright.php file in the file structure
9
 * ******************************************************************************
10
 */
11
12
namespace core\common;
13
14
/**
15
 * This class contains a number of functions for talking to the outside world
16
 * @author Stefan Winter <[email protected]>
17
 * @author Tomasz Wolniewicz <[email protected]>
18
 *
19
 * @package Developer
20
 */
21
class OutsideComm {
22
23
    /**
24
     * downloads a file from the internet
25
     * @param string $url
26
     * @return string|boolean the data we got back, or FALSE on failure
27
     */
28
    public static function downloadFile($url) {
29
        $loggerInstance = new \core\common\Logging();
30
        if (!preg_match("/:\/\//", $url)) {
31
            $loggerInstance->debug(3, "The specified string does not seem to be a URL!");
32
            return FALSE;
33
        }
34
        # we got a URL, download it
35
        $download = fopen($url, "rb");
36
        if ($download === FALSE) {
37
            $loggerInstance->debug(2, "Failed to open handle for $url");
38
            return FALSE;
39
        }
40
        $data = stream_get_contents($download);
41
        if ($data === FALSE) {
42
            $loggerInstance->debug(2, "Failed to download the file from $url");
43
            return FALSE;
44
        }
45
        return $data;
46
    }
47
48
    /**
49
     * create an email handle from PHPMailer for later customisation and sending
50
     * @return \PHPMailer\PHPMailer\PHPMailer
51
     */
52
    public static function mailHandle() {
53
// use PHPMailer to send the mail
54
        $mail = new \PHPMailer\PHPMailer\PHPMailer();
55
        $mail->isSMTP();
56
        $mail->SMTPAuth = true;
57
        $mail->Port = 587;
58
        $mail->SMTPSecure = 'tls';
59
        $mail->Host = CONFIG['MAILSETTINGS']['host'];
60
        $mail->Username = CONFIG['MAILSETTINGS']['user'];
61
        $mail->Password = CONFIG['MAILSETTINGS']['pass'];
62
// formatting nitty-gritty
63
        $mail->WordWrap = 72;
64
        $mail->isHTML(FALSE);
65
        $mail->CharSet = 'UTF-8';
66
        $mail->From = CONFIG['APPEARANCE']['from-mail'];
67
// are we fancy? i.e. S/MIME signing?
68
        if (isset(CONFIG['MAILSETTINGS']['certfilename'], CONFIG['MAILSETTINGS']['keyfilename'], CONFIG['MAILSETTINGS']['keypass'])) {
69
            $mail->sign(CONFIG['MAILSETTINGS']['certfilename'], CONFIG['MAILSETTINGS']['keyfilename'], CONFIG['MAILSETTINGS']['keypass']);
70
        }
71
        return $mail;
72
    }
73
74
    const MAILDOMAIN_INVALID = -1000;
75
    const MAILDOMAIN_NO_MX = -1001;
76
    const MAILDOMAIN_NO_HOST = -1002;
77
    const MAILDOMAIN_NO_CONNECT = -1003;
78
    const MAILDOMAIN_NO_STARTTLS = 1;
79
    const MAILDOMAIN_STARTTLS = 2;
80
81
    /**
82
     * verifies whether a mail address is in an existing and STARTTLS enabled mail domain
83
     * @param string $address
84
     * @return int status of the mail domain
85
     */
86
    public static function mailAddressValidSecure($address) {
87
        $loggerInstance = new \core\common\Logging();
88
        if (!filter_var($address, FILTER_VALIDATE_EMAIL)) {
89
            $loggerInstance->debug(4, "OutsideComm::mailAddressValidSecure: invalid mail address.");
90
            return OutsideComm::MAILDOMAIN_INVALID;
91
        }
92
        $domain = substr($address, strpos($address, '@') + 1); // everything after the @ sign
93
        // we can be sure that the @ was found (FILTER_VALIDATE_EMAIL succeeded)
94
        // but let's be explicit
95
        if ($domain === FALSE) {
96
            return OutsideComm::MAILDOMAIN_INVALID;
97
        }
98
        // does the domain have MX records?
99
        $mx = dns_get_record($domain, DNS_MX);
100
        if ($mx === FALSE) {
101
            $loggerInstance->debug(4, "OutsideComm::mailAddressValidSecure: no MX.");
102
            return OutsideComm::MAILDOMAIN_NO_MX;
103
        }
104
        $loggerInstance->debug(5, "Domain: $domain MX: " . print_r($mx, TRUE));
105
        // create a pool of A and AAAA records for all the MXes
106
        $ipAddrs = [];
107
        foreach ($mx as $onemx) {
108
            $v4list = dns_get_record($onemx['target'], DNS_A);
109
            $v6list = dns_get_record($onemx['target'], DNS_AAAA);
110
            foreach ($v4list as $oneipv4) {
111
                $ipAddrs[] = $oneipv4['ip'];
112
            }
113
            foreach ($v6list as $oneipv6) {
114
                $ipAddrs[] = "[" . $oneipv6['ipv6'] . "]";
115
            }
116
        }
117
        if (count($ipAddrs) == 0) {
118
            $loggerInstance->debug(4, "OutsideComm::mailAddressValidSecure: no mailserver hosts.");
119
            return OutsideComm::MAILDOMAIN_NO_HOST;
120
        }
121
        $loggerInstance->debug(5, "Domain: $domain Addrs: " . print_r($ipAddrs, TRUE));
122
        // connect to all hosts. If all can't connect, return MAILDOMAIN_NO_CONNECT. 
123
        // If at least one does not support STARTTLS or one of the hosts doesn't connect
124
        // , return MAILDOMAIN_NO_STARTTLS (one which we can't connect to we also
125
        // can't verify if it's doing STARTTLS, so better safe than sorry.
126
        $retval = OutsideComm::MAILDOMAIN_NO_CONNECT;
127
        $allWithStarttls = TRUE;
128
        foreach ($ipAddrs as $oneip) {
129
            $loggerInstance->debug(5, "OutsideComm::mailAddressValidSecure: connecting to $oneip.");
130
            $smtp = new \PHPMailer\PHPMailer\SMTP;
131
            if ($smtp->connect($oneip, 25)) {
132
                // host reached! so at least it's not a NO_CONNECT
133
                $loggerInstance->debug(4, "OutsideComm::mailAddressValidSecure: connected to $oneip.");
134
                $retval = OutsideComm::MAILDOMAIN_NO_STARTTLS;
135
                if ($smtp->hello('eduroam.org')) {
136
                    $extensions = $smtp->getServerExtList(); // Scrutinzer is wrong; is not always null - contains server capabilities
137
                    if (!is_array($extensions) || !array_key_exists('STARTTLS', $extensions)) {
138
                        $loggerInstance->debug(4, "OutsideComm::mailAddressValidSecure: no indication for STARTTLS.");
139
                        $allWithStarttls = FALSE;
140
                    }
141
                }
142
            } else {
143
                // no connect: then we can't claim all targets have STARTTLS
144
                $allWithStarttls = FALSE;
145
                $loggerInstance->debug(5, "OutsideComm::mailAddressValidSecure: failed $oneip.");
146
            }
147
        }
148
        // did the state $allWithStarttls survive? Then up the response to
149
        // appropriate level.
150
        if ($retval == OutsideComm::MAILDOMAIN_NO_STARTTLS && $allWithStarttls) {
151
            $retval = OutsideComm::MAILDOMAIN_STARTTLS;
152
        }
153
        return $retval;
154
    }
155
156
    const SMS_SENT = 100;
157
    const SMS_NOTSENT = 101;
158
    const SMS_FRAGEMENTSLOST = 102;
159
160
    /**
161
     * Send SMS invitations to end users
162
     * 
163
     * @param string $number the number to send to: with country prefix, but without the + sign ("352123456" for a Luxembourg example)
164
     * @param string $content the text to send
165
     * @return integer status of the sending process
166
     * @throws Exception
167
     */
168
    public static function sendSMS($number, $content) {
169
        $loggerInstance = new \core\common\Logging();
170
        switch (CONFIG_CONFASSISTANT['SMSSETTINGS']['provider']) {
171
            case 'Nexmo':
172
                // taken from https://docs.nexmo.com/messaging/sms-api
173
                $url = 'https://rest.nexmo.com/sms/json?' . http_build_query(
174
                                [
175
                                    'api_key' => CONFIG_CONFASSISTANT['SMSSETTINGS']['username'],
176
                                    'api_secret' => CONFIG_CONFASSISTANT['SMSSETTINGS']['password'],
177
                                    'to' => $number,
178
                                    'from' => CONFIG['APPEARANCE']['productname'],
179
                                    'text' => $content,
180
                                ]
181
                );
182
183
                $ch = curl_init($url);
184
                curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
185
                $response = curl_exec($ch);
186
187
                $decoded_response = json_decode($response, true);
188
                $messageCount = $decoded_response['message-count'];
189
190
                if ($messageCount == 0) {
191
                    $loggerInstance->debug(2, 'Problem with SMS invitation: no message was sent!');
192
                    return OutsideComm::SMS_NOTSENT;
193
                }
194
                $loggerInstance->debug(2, 'Total of ' . $messageCount . ' messages were attempted to send.');
195
196
                $totalFailures = 0;
197
                foreach ($decoded_response['messages'] as $message) {
198
                    if ($message['status'] == 0) {
199
                        $loggerInstance->debug(2, $message['message-id'] . ": Success");
200
                    } else {
201
                        $loggerInstance->debug(2, $message['message-id'] . ": Failed (failure code = " . $message['status'] . ")");
202
                        $totalFailures++;
203
                    }
204
                }
205
                if ($messageCount == count($decoded_response['messages']) && $totalFailures == 0) {
206
                    return OutsideComm::SMS_SENT;
207
                }
208
                return OutsideComm::SMS_FRAGEMENTSLOST;
209
            default:
210
                throw new \Exception("Unknown SMS Gateway provider!");
211
        }
212
    }
213
214
    const INVITE_CONTEXTS = [
215
        0 => "CO-ADMIN",
216
        1 => "NEW-FED",
217
        2 => "EXISTING-FED",
218
    ];
219
220
    /**
221
     * 
222
     * @param string $targets one or more mail addresses, comma-separated
223
     * @param string $introtext introductory sentence (varies by situation)
224
     * @param string $newtoken the token to send
225
     * @param \core\Federation $federation if not NULL, indicates that invitation comes from authorised fed admin of that federation
226
     * @return array
227
     */
228
    public static function adminInvitationMail($targets, $introtext, $newtoken, $idpPrettyName, $federation) {
229
        if (!in_array($introtext, OutsideComm::INVITE_CONTEXTS)) {
230
            throw new \Exception("Unknown invite mode!");
231
        }
232
        if ($introtext == OutsideComm::INVITE_CONTEXTS[1] && $federation === NULL) { // comes from fed admin, so federation must be set
233
            throw new \Exception("Invitation from a fed admin, but we do not know the corresponding federation!");
234
        }
235
        $mail = OutsideComm::mailHandle();
236
        $cat = new \core\CAT();
237
        // we have a few stock intro texts on file
238
        $introTexts = [
239
            OutsideComm::INVITE_CONTEXTS[0] => sprintf(_("a %s of the %s %s \"%s\" has invited you to manage the %s together with him."), $cat->nomenclature_fed, CONFIG_CONFASSISTANT['CONSORTIUM']['display_name'], $cat->nomenclature_inst, $idpPrettyName, $cat->nomenclature_inst),
240
            OutsideComm::INVITE_CONTEXTS[1] => sprintf(_("a %s %s has invited you to manage the future %s  \"%s\" (%s)."), CONFIG_CONFASSISTANT['CONSORTIUM']['display_name'], $cat->nomenclature_fed, $cat->nomenclature_inst, $idpPrettyName, strtoupper($federation->tld)),
241
            OutsideComm::INVITE_CONTEXTS[2] => sprintf(_("a %s %s has invited you to manage the %s  \"%s\"."), CONFIG_CONFASSISTANT['CONSORTIUM']['display_name'], $cat->nomenclature_fed, $cat->nomenclature_inst, $idpPrettyName),
242
        ];
243
        $validity = sprintf(_("This invitation is valid for 24 hours from now, i.e. until %s."), strftime("%x %X", time() + 86400));
244
        // need some nomenclature
245
        // are we on https?
246
        $proto = "http://";
247
        if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == "on") {
248
            $proto = "https://";
249
        }
250
        // then, send out the mail
251
        $message = _("Hello,") . "\n\n" . wordwrap($introTexts[$introtext] . " " . $validity, 72) . "\n\n";
252
        // default means we don't have a Reply-To.
253
        $replyToMessage = wordwrap(_("manually. Please do not reply to this mail; this is a send-only address."));
254
255
        if ($federation !== NULL) {
256
            // see if we are supposed to add a custom message
257
            $customtext = $federation->getAttributes('fed:custominvite');
258
            if (count($customtext) > 0) {
259
                $message .= wordwrap(sprintf(_("Additional message from your %s administrator:"), $cat->nomenclature_fed), 72) . "\n---------------------------------" .
260
                        wordwrap($customtext[0]['value'], 72) . "\n---------------------------------\n\n";
261
            }
262
            // and add Reply-To already now
263
            foreach ($federation->listFederationAdmins() as $fedadmin_id) {
264
                $fedadmin = new \core\User($fedadmin_id);
265
                $mailaddrAttrib = $fedadmin->getAttributes("user:email");
266
                $nameAttrib = $fedadmin->getAttributes("user:realname");
267
                $name = $nameAttrib[0]['value'] ?? sprintf(_("%s administrator"), $cat->nomenclature_fed);
268
                if (count($mailaddrAttrib) > 0) {
269
                    $mail->addReplyTo($mailaddrAttrib[0]['value'], $name);
270
                    $replyToMessage = wordwrap(sprintf(_("manually. If you reply to this mail, you will reach your %s administrators."), $cat->nomenclature_fed), 72);
271
                }
272
            }
273
        }
274
275
        $message .= wordwrap(sprintf(_("To enlist as an administrator for that %s, please click on the following link:"), $cat->nomenclature_inst), 72) . "\n\n" .
276
                $proto . $_SERVER['SERVER_NAME'] . dirname(dirname($_SERVER['SCRIPT_NAME'])) . "/admin/action_enrollment.php?token=$newtoken\n\n" .
277
                wordwrap(sprintf(_("If clicking the link doesn't work, you can also go to the %s Administrator Interface at"), CONFIG['APPEARANCE']['productname']), 72) . "\n\n" .
278
                $proto . $_SERVER['SERVER_NAME'] . dirname(dirname($_SERVER['SCRIPT_NAME'])) . "/admin/\n\n" .
279
                _("and enter the invitation token") . "\n\n" .
280
                $newtoken . "\n\n$replyToMessage\n\n" .
281
                wordwrap(_("Do NOT forward the mail before the token has expired - or the recipients may be able to consume the token on your behalf!"), 72) . "\n\n" .
282
                wordwrap(sprintf(_("We wish you a lot of fun with the %s."), CONFIG['APPEARANCE']['productname']), 72) . "\n\n" .
283
                sprintf(_("Sincerely,\n\nYour friendly folks from %s Operations"), CONFIG_CONFASSISTANT['CONSORTIUM']['display_name']);
284
285
286
// who to whom?
287
        $mail->FromName = CONFIG['APPEARANCE']['productname'] . " Invitation System";
288
289
        if (isset(CONFIG['APPEARANCE']['invitation-bcc-mail']) && CONFIG['APPEARANCE']['invitation-bcc-mail'] !== NULL) {
290
            $mail->addBCC(CONFIG['APPEARANCE']['invitation-bcc-mail']);
291
        }
292
293
// all addresses are wrapped in a string, but PHPMailer needs a structured list of addressees
294
// sigh... so convert as needed
295
// first split multiple into one if needed
296
        $recipients = explode(", ", $targets);
297
298
        $secStatus = TRUE;
299
        $domainStatus = TRUE;
300
301
// fill the destinations in PHPMailer API
302
        foreach ($recipients as $recipient) {
303
            $mail->addAddress($recipient);
304
            $status = OutsideComm::mailAddressValidSecure($recipient);
305
            if ($status < OutsideComm::MAILDOMAIN_STARTTLS) {
306
                $secStatus = FALSE;
307
            }
308
            if ($status < 0) {
309
                $domainStatus = FALSE;
310
            }
311
        }
312
313
        if (!$domainStatus) {
314
            return ["SENT" => FALSE, "TRANSPORT" => FALSE];
315
        }
316
317
// what do we want to say?
318
        $mail->Subject = sprintf(_("%s: you have been invited to manage an %s"), CONFIG['APPEARANCE']['productname'], $cat->nomenclature_inst);
319
        $mail->Body = $message;
320
321
        return ["SENT" => $mail->send(), "TRANSPORT" => $secStatus];
322
    }
323
324
    public static function postJson($url, $dataArray) {
325
        $ch = \curl_init($url);
326
        \curl_setopt_array($ch, array(
327
            CURLOPT_POST => TRUE,
328
            CURLOPT_RETURNTRANSFER => TRUE,
329
            CURLOPT_POSTFIELDS => json_encode($dataArray),
330
            CURLOPT_FRESH_CONNECT => TRUE,
331
        ));
332
        $response = \curl_exec($ch);
333
        if ($response === FALSE || $response === NULL) {
334
            throw new Exception("the POST didn't work!");
0 ignored issues
show
Bug introduced by
The type core\common\Exception was not found. Did you mean Exception? If so, make sure to prefix the type with \.
Loading history...
335
        }
336
        return json_decode($response, TRUE);
337
    }
338
339
}
340