Passed
Push — master ( d92470...59cfc2 )
by Stefan
10:53
created

OutsideComm::mailAddressValidSecure()   C

Complexity

Conditions 15
Paths 58

Size

Total Lines 68
Code Lines 43

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 15
eloc 43
nc 58
nop 1
dl 0
loc 68
rs 5.9166
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/*
4
 * *****************************************************************************
5
 * Contributions to this work were made on behalf of the GÉANT project, a 
6
 * project that has received funding from the European Union’s Framework 
7
 * Programme 7 under Grant Agreements No. 238875 (GN3) and No. 605243 (GN3plus),
8
 * Horizon 2020 research and innovation programme under Grant Agreements No. 
9
 * 691567 (GN4-1) and No. 731122 (GN4-2).
10
 * On behalf of the aforementioned projects, GEANT Association is the sole owner
11
 * of the copyright in all material which was developed by a member of the GÉANT
12
 * project. GÉANT Vereniging (Association) is registered with the Chamber of 
13
 * Commerce in Amsterdam with registration number 40535155 and operates in the 
14
 * UK as a branch of GÉANT Vereniging.
15
 * 
16
 * Registered office: Hoekenrode 3, 1102BR Amsterdam, The Netherlands. 
17
 * UK branch address: City House, 126-130 Hills Road, Cambridge CB2 1PQ, UK
18
 *
19
 * License: see the web/copyright.inc.php file in the file structure or
20
 *          <base_url>/copyright.php after deploying the software
21
 */
22
23
namespace core\common;
24
25
/**
26
 * This class contains a number of functions for talking to the outside world
27
 * @author Stefan Winter <[email protected]>
28
 * @author Tomasz Wolniewicz <[email protected]>
29
 *
30
 * @package Developer
31
 */
32
class OutsideComm extends Entity {
33
34
    /**
35
     * downloads a file from the internet
36
     * @param string $url the URL to download
37
     * @return string|boolean the data we got back, or FALSE on failure
38
     */
39
    public static function downloadFile($url) {
40
        $loggerInstance = new \core\common\Logging();
41
        if (!preg_match("/:\/\//", $url)) {
42
            $loggerInstance->debug(3, "The specified string does not seem to be a URL!");
43
            return FALSE;
44
        }
45
        # we got a URL, download it
46
        $download = fopen($url, "rb");
47
        if ($download === FALSE) {
48
            $loggerInstance->debug(2, "Failed to open handle for $url");
49
            return FALSE;
50
        }
51
        $data = stream_get_contents($download);
52
        if ($data === FALSE) {
53
            $loggerInstance->debug(2, "Failed to download the file from $url");
54
            return FALSE;
55
        }
56
        return $data;
57
    }
58
59
    /**
60
     * create an email handle from PHPMailer for later customisation and sending
61
     * @return \PHPMailer\PHPMailer\PHPMailer
62
     */
63
    public static function mailHandle() {
64
// use PHPMailer to send the mail
65
        $mail = new \PHPMailer\PHPMailer\PHPMailer();
66
        $mail->isSMTP();
67
        $mail->Port = 587;
68
        $mail->SMTPSecure = 'tls';
69
        $mail->Host = \config\Master::CONFIG['MAILSETTINGS']['host'];
70
        if (\config\Master::CONFIG['MAILSETTINGS']['user'] === NULL && \config\Master::CONFIG['MAILSETTINGS']['pass'] === NULL) {
71
            $mail->SMTPAuth = false;
72
        } else {
73
            $mail->SMTPAuth = true;
74
            $mail->Username = \config\Master::CONFIG['MAILSETTINGS']['user'];
75
            $mail->Password = \config\Master::CONFIG['MAILSETTINGS']['pass'];
76
        }
77
        $mail->SMTPOptions = \config\Master::CONFIG['MAILSETTINGS']['options'];
78
// formatting nitty-gritty
79
        $mail->WordWrap = 72;
80
        $mail->isHTML(FALSE);
81
        $mail->CharSet = 'UTF-8';
82
        $mail->From = \config\Master::CONFIG['APPEARANCE']['from-mail'];
83
// are we fancy? i.e. S/MIME signing?
84
        if (isset(\config\Master::CONFIG['MAILSETTINGS']['certfilename'], \config\Master::CONFIG['MAILSETTINGS']['keyfilename'], \config\Master::CONFIG['MAILSETTINGS']['keypass'])) {
85
            $mail->sign(\config\Master::CONFIG['MAILSETTINGS']['certfilename'], \config\Master::CONFIG['MAILSETTINGS']['keyfilename'], \config\Master::CONFIG['MAILSETTINGS']['keypass']);
86
        }
87
        return $mail;
88
    }
89
90
    const MAILDOMAIN_INVALID = -1000;
91
    const MAILDOMAIN_NO_MX = -1001;
92
    const MAILDOMAIN_NO_HOST = -1002;
93
    const MAILDOMAIN_NO_CONNECT = -1003;
94
    const MAILDOMAIN_NO_STARTTLS = 1;
95
    const MAILDOMAIN_STARTTLS = 2;
96
97
    /**
98
     * verifies whether a mail address is in an existing and STARTTLS enabled mail domain
99
     * 
100
     * @param string $address the mail address to check
101
     * @return int status of the mail domain
102
     */
103
    public static function mailAddressValidSecure($address) {
104
        $loggerInstance = new \core\common\Logging();
105
        if (!filter_var($address, FILTER_VALIDATE_EMAIL)) {
106
            $loggerInstance->debug(4, "OutsideComm::mailAddressValidSecure: invalid mail address.");
107
            return OutsideComm::MAILDOMAIN_INVALID;
108
        }
109
        $domain = substr($address, strpos($address, '@') + 1); // everything after the @ sign
110
        // we can be sure that the @ was found (FILTER_VALIDATE_EMAIL succeeded)
111
        // but let's be explicit
112
        if ($domain === FALSE) {
113
            return OutsideComm::MAILDOMAIN_INVALID;
114
        }
115
        // does the domain have MX records?
116
        $mx = dns_get_record($domain, DNS_MX);
117
        if ($mx === FALSE) {
118
            $loggerInstance->debug(4, "OutsideComm::mailAddressValidSecure: no MX.");
119
            return OutsideComm::MAILDOMAIN_NO_MX;
120
        }
121
        $loggerInstance->debug(5, "Domain: $domain MX: " . print_r($mx, TRUE));
122
        // create a pool of A and AAAA records for all the MXes
123
        $ipAddrs = [];
124
        foreach ($mx as $onemx) {
125
            $v4list = dns_get_record($onemx['target'], DNS_A);
126
            $v6list = dns_get_record($onemx['target'], DNS_AAAA);
127
            foreach ($v4list as $oneipv4) {
128
                $ipAddrs[] = $oneipv4['ip'];
129
            }
130
            foreach ($v6list as $oneipv6) {
131
                $ipAddrs[] = "[" . $oneipv6['ipv6'] . "]";
132
            }
133
        }
134
        if (count($ipAddrs) == 0) {
135
            $loggerInstance->debug(4, "OutsideComm::mailAddressValidSecure: no mailserver hosts.");
136
            return OutsideComm::MAILDOMAIN_NO_HOST;
137
        }
138
        $loggerInstance->debug(5, "Domain: $domain Addrs: " . print_r($ipAddrs, TRUE));
139
        // connect to all hosts. If all can't connect, return MAILDOMAIN_NO_CONNECT. 
140
        // If at least one does not support STARTTLS or one of the hosts doesn't connect
141
        // , return MAILDOMAIN_NO_STARTTLS (one which we can't connect to we also
142
        // can't verify if it's doing STARTTLS, so better safe than sorry.
143
        $retval = OutsideComm::MAILDOMAIN_NO_CONNECT;
144
        $allWithStarttls = TRUE;
145
        foreach ($ipAddrs as $oneip) {
146
            $loggerInstance->debug(5, "OutsideComm::mailAddressValidSecure: connecting to $oneip.");
147
            $smtp = new \PHPMailer\PHPMailer\SMTP;
148
            if ($smtp->connect($oneip, 25)) {
149
                // host reached! so at least it's not a NO_CONNECT
150
                $loggerInstance->debug(4, "OutsideComm::mailAddressValidSecure: connected to $oneip.");
151
                $retval = OutsideComm::MAILDOMAIN_NO_STARTTLS;
152
                if ($smtp->hello('eduroam.org')) {
153
                    $extensions = $smtp->getServerExtList(); // Scrutinzer is wrong; is not always null - contains server capabilities
154
                    if (!is_array($extensions) || !array_key_exists('STARTTLS', $extensions)) {
155
                        $loggerInstance->debug(4, "OutsideComm::mailAddressValidSecure: no indication for STARTTLS.");
156
                        $allWithStarttls = FALSE;
157
                    }
158
                }
159
            } else {
160
                // no connect: then we can't claim all targets have STARTTLS
161
                $allWithStarttls = FALSE;
162
                $loggerInstance->debug(5, "OutsideComm::mailAddressValidSecure: failed $oneip.");
163
            }
164
        }
165
        // did the state $allWithStarttls survive? Then up the response to
166
        // appropriate level.
167
        if ($retval == OutsideComm::MAILDOMAIN_NO_STARTTLS && $allWithStarttls) {
168
            $retval = OutsideComm::MAILDOMAIN_STARTTLS;
169
        }
170
        return $retval;
171
    }
172
173
    const SMS_SENT = 100;
174
    const SMS_NOTSENT = 101;
175
    const SMS_FRAGEMENTSLOST = 102;
176
177
    /**
178
     * Send SMS invitations to end users
179
     * 
180
     * @param string $number  the number to send to: with country prefix, but without the + sign ("352123456" for a Luxembourg example)
181
     * @param string $content the text to send
182
     * @return integer status of the sending process
183
     * @throws \Exception
184
     */
185
    public static function sendSMS($number, $content) {
186
        $loggerInstance = new \core\common\Logging();
187
        switch (\config\ConfAssistant::CONFIG['SMSSETTINGS']['provider']) {
188
            case 'Nexmo':
189
                // taken from https://docs.nexmo.com/messaging/sms-api
190
                $url = 'https://rest.nexmo.com/sms/json?' . http_build_query(
191
                                [
192
                                    'api_key' => \config\ConfAssistant::CONFIG['SMSSETTINGS']['username'],
193
                                    'api_secret' => \config\ConfAssistant::CONFIG['SMSSETTINGS']['password'],
194
                                    'to' => $number,
195
                                    'from' => \config\ConfAssistant::CONFIG['CONSORTIUM']['name'],
196
                                    'text' => $content,
197
                                    'type' => 'unicode',
198
                                ]
199
                );
200
201
                $ch = curl_init($url);
202
                if ($ch === FALSE) {
203
                    $loggerInstance->debug(2, 'Problem with SMS invitation: unable to send API request with CURL!');
204
                    return OutsideComm::SMS_NOTSENT;
205
                }
206
                curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
207
                $response = curl_exec($ch);
208
209
                $decoded_response = json_decode($response, true);
210
                $messageCount = $decoded_response['message-count'];
211
212
                if ($messageCount == 0) {
213
                    $loggerInstance->debug(2, 'Problem with SMS invitation: no message was sent!');
214
                    return OutsideComm::SMS_NOTSENT;
215
                }
216
                $loggerInstance->debug(2, 'Total of ' . $messageCount . ' messages were attempted to send.');
217
218
                $totalFailures = 0;
219
                foreach ($decoded_response['messages'] as $message) {
220
                    if ($message['status'] == 0) {
221
                        $loggerInstance->debug(2, $message['message-id'] . ": Success");
222
                    } else {
223
                        $loggerInstance->debug(2, $message['message-id'] . ": Failed (failure code = " . $message['status'] . ")");
224
                        $totalFailures++;
225
                    }
226
                }
227
                if ($messageCount == count($decoded_response['messages']) && $totalFailures == 0) {
228
                    return OutsideComm::SMS_SENT;
229
                }
230
                return OutsideComm::SMS_FRAGEMENTSLOST;
231
            default:
232
                throw new \Exception("Unknown SMS Gateway provider!");
233
        }
234
    }
235
236
    const INVITE_CONTEXTS = [
237
        0 => "CO-ADMIN",
238
        1 => "NEW-FED",
239
        2 => "EXISTING-FED",
240
    ];
241
242
    /**
243
     * 
244
     * @param string           $targets       one or more mail addresses, comma-separated
245
     * @param string           $introtext     introductory sentence (varies by situation)
246
     * @param string           $newtoken      the token to send
247
     * @param string           $idpPrettyName the name of the IdP, in best-match language
248
     * @param \core\Federation $federation    if not NULL, indicates that invitation comes from authorised fed admin of that federation
249
     * @param string           $type          the type of participant we're invited to
250
     * @return array
251
     * @throws \Exception
252
     */
253
    public static function adminInvitationMail($targets, $introtext, $newtoken, $idpPrettyName, $federation, $type) {
254
        if (!in_array($introtext, OutsideComm::INVITE_CONTEXTS)) {
255
            throw new \Exception("Unknown invite mode!");
256
        }
257
        if ($introtext == OutsideComm::INVITE_CONTEXTS[1] && $federation === NULL) { // comes from fed admin, so federation must be set
258
            throw new \Exception("Invitation from a fed admin, but we do not know the corresponding federation!");
259
        }
260
        $prettyPrintType = "";
261
        switch ($type) {
262
            case \core\IdP::TYPE_IDP:
263
                $prettyPrintType = Entity::$nomenclature_inst;
264
                break;
265
            case \core\IdP::TYPE_SP:
266
                $prettyPrintType = Entity::$nomenclature_hotspot;
267
                break;
268
            case \core\IdP::TYPE_IDPSP:
269
                $prettyPrintType = sprintf(_("%s and %s"), Entity::$nomenclature_inst, Entity::$nomenclature_hotspot);
270
                break;
271
            default:
272
                throw new \Exception("This is controlled vocabulary, impossible.");
273
        }
274
        Entity::intoThePotatoes();
275
        $mail = OutsideComm::mailHandle();
276
        new \core\CAT(); // makes sure Entity is initialised
277
        // we have a few stock intro texts on file
278
        $introTexts = [
279
            OutsideComm::INVITE_CONTEXTS[0] => sprintf(_("a %s of the %s %s \"%s\" has invited you to manage the %s together with him."), Entity::$nomenclature_fed, \config\ConfAssistant::CONFIG['CONSORTIUM']['display_name'], Entity::$nomenclature_participant, $idpPrettyName, Entity::$nomenclature_participant),
280
            OutsideComm::INVITE_CONTEXTS[1] => sprintf(_("a %s %s has invited you to manage the future %s  \"%s\" (%s). The organisation will be a %s."), \config\ConfAssistant::CONFIG['CONSORTIUM']['display_name'], Entity::$nomenclature_fed, Entity::$nomenclature_participant, $idpPrettyName, strtoupper($federation->tld), $prettyPrintType),
281
            OutsideComm::INVITE_CONTEXTS[2] => sprintf(_("a %s %s has invited you to manage the %s  \"%s\". This is a %s."), \config\ConfAssistant::CONFIG['CONSORTIUM']['display_name'], Entity::$nomenclature_fed, Entity::$nomenclature_participant, $idpPrettyName, $prettyPrintType),
282
        ];
283
        $validity = sprintf(_("This invitation is valid for 24 hours from now, i.e. until %s."), strftime("%x %X %Z", time() + 86400));
284
        // need some nomenclature
285
        // are we on https?
286
        $proto = "http://";
287
        if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == "on") {
288
            $proto = "https://";
289
        }
290
        // then, send out the mail
291
        $message = _("Hello,") . "\n\n" . wordwrap($introTexts[$introtext] . " " . $validity, 72) . "\n\n";
292
        // default means we don't have a Reply-To.
293
        $replyToMessage = wordwrap(_("manually. Please do not reply to this mail; this is a send-only address."));
294
295
        if ($federation !== NULL) {
296
            // see if we are supposed to add a custom message
297
            $customtext = $federation->getAttributes('fed:custominvite');
298
            if (count($customtext) > 0) {
299
                $message .= wordwrap(sprintf(_("Additional message from your %s administrator:"), Entity::$nomenclature_fed), 72) . "\n---------------------------------" .
300
                        wordwrap($customtext[0]['value'], 72) . "\n---------------------------------\n\n";
301
            }
302
            // and add Reply-To already now
303
            foreach ($federation->listFederationAdmins() as $fedadmin_id) {
304
                $fedadmin = new \core\User($fedadmin_id);
305
                $mailaddrAttrib = $fedadmin->getAttributes("user:email");
306
                $nameAttrib = $fedadmin->getAttributes("user:realname");
307
                $name = $nameAttrib[0]['value'] ?? sprintf(_("%s administrator"), Entity::$nomenclature_fed);
308
                if (count($mailaddrAttrib) > 0) {
309
                    $mail->addReplyTo($mailaddrAttrib[0]['value'], $name);
310
                    $replyToMessage = wordwrap(sprintf(_("manually. If you reply to this mail, you will reach your %s administrators."), Entity::$nomenclature_fed), 72);
311
                }
312
            }
313
        }
314
315
        $message .= wordwrap(sprintf(_("To enlist as an administrator for that %s, please click on the following link:"), Entity::$nomenclature_participant), 72) . "\n\n" .
316
                $proto . $_SERVER['SERVER_NAME'] . \config\Master::CONFIG['PATHS']['cat_base_url'] . "admin/action_enrollment.php?token=$newtoken\n\n" .
317
                wordwrap(sprintf(_("If clicking the link doesn't work, you can also go to the %s Administrator Interface at"), \config\Master::CONFIG['APPEARANCE']['productname']), 72) . "\n\n" .
318
                $proto . $_SERVER['SERVER_NAME'] . \config\Master::CONFIG['PATHS']['cat_base_url'] . "admin/\n\n" .
319
                _("and enter the invitation token") . "\n\n" .
320
                $newtoken . "\n\n$replyToMessage\n\n" .
321
                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" .
322
                wordwrap(sprintf(_("We wish you a lot of fun with the %s."), \config\Master::CONFIG['APPEARANCE']['productname']), 72) . "\n\n" .
323
                sprintf(_("Sincerely,\n\nYour friendly folks from %s Operations"), \config\ConfAssistant::CONFIG['CONSORTIUM']['display_name']);
324
325
326
// who to whom?
327
        $mail->FromName = \config\Master::CONFIG['APPEARANCE']['productname'] . " Invitation System";
328
329
        if (isset(\config\Master::CONFIG['APPEARANCE']['invitation-bcc-mail']) && \config\Master::CONFIG['APPEARANCE']['invitation-bcc-mail'] !== NULL) {
330
            $mail->addBCC(\config\Master::CONFIG['APPEARANCE']['invitation-bcc-mail']);
331
        }
332
333
// all addresses are wrapped in a string, but PHPMailer needs a structured list of addressees
334
// sigh... so convert as needed
335
// first split multiple into one if needed
336
        $recipients = explode(", ", $targets);
337
338
        $secStatus = TRUE;
339
        $domainStatus = TRUE;
340
341
// fill the destinations in PHPMailer API
342
        foreach ($recipients as $recipient) {
343
            $mail->addAddress($recipient);
344
            $status = OutsideComm::mailAddressValidSecure($recipient);
345
            if ($status < OutsideComm::MAILDOMAIN_STARTTLS) {
346
                $secStatus = FALSE;
347
            }
348
            if ($status < 0) {
349
                $domainStatus = FALSE;
350
            }
351
        }
352
        Entity::outOfThePotatoes();
353
        if (!$domainStatus) {
354
            return ["SENT" => FALSE, "TRANSPORT" => FALSE];
355
        }
356
357
// what do we want to say?
358
        Entity::intoThePotatoes();
359
        $mail->Subject = sprintf(_("%s: you have been invited to manage an %s"), \config\Master::CONFIG['APPEARANCE']['productname'], Entity::$nomenclature_participant);
360
        Entity::outOfThePotatoes();
361
        $mail->Body = $message;
362
        return ["SENT" => $mail->send(), "TRANSPORT" => $secStatus];
363
    }
364
365
    /**
366
     * sends a POST with some JSON inside
367
     * 
368
     * @param string $url       the URL to POST to
369
     * @param array  $dataArray the data to be sent in PHP array representation
370
     * @return array the JSON response, decoded into PHP associative array
371
     * @throws \Exception
372
     */
373
    public static function postJson($url, $dataArray) {
374
        $loggerInstance = new Logging();
375
        $ch = \curl_init($url);
376
        if ($ch === FALSE) {
377
            $loggerInstance->debug(2, "Unable to POST JSON request: CURL init failed!");
378
            return json_decode(json_encode(FALSE), TRUE);
379
        }
380
        \curl_setopt_array($ch, array(
381
            CURLOPT_POST => TRUE,
382
            CURLOPT_RETURNTRANSFER => TRUE,
383
            CURLOPT_POSTFIELDS => json_encode($dataArray),
384
            CURLOPT_FRESH_CONNECT => TRUE,
385
        ));
386
        $response = \curl_exec($ch);
387
        if ($response === FALSE || $response === NULL || $response === TRUE) { // With RETURNTRANSFER, TRUE is not a valid return
388
            throw new \Exception("the POST didn't work!");
389
        }
390
        return json_decode($response, TRUE);
391
    }
392
393
    /**
394
     * aborts code execution if a required mail address is invalid
395
     * 
396
     * @param mixed  $newmailaddress       input string, possibly one or more mail addresses
397
     * @param string $redirect_destination destination to send user to if validation failed
0 ignored issues
show
Coding Style introduced by
Superfluous parameter comment
Loading history...
398
     * @return array mail address if validation passed
399
     */
400
    public static function exfiltrateValidAddresses($newmailaddress) {
401
        $validator = new \web\lib\common\InputValidation();
402
        $addressSegments = explode(",", $newmailaddress);
403
        $confirmedMails = [];
404
        if ($addressSegments === FALSE) {
405
            return [];
406
        }
407
        foreach ($addressSegments as $oneAddressCandidate) {
408
            $candidate = trim($oneAddressCandidate);
409
            if ($validator->email($candidate) !== FALSE) {
410
                $confirmedMails[] = $candidate;
411
            }
412
        }
413
        if (count($confirmedMails) == 0) {
414
            return [];
415
        }
416
        return $confirmedMails;
417
    }
418
419
    /**
420
     * performs an HTTP request. Currently unused, will be for external CA API calls.
421
     * 
422
     * @param string $url the URL to send the request to
423
     * @param array $postValues POST values to send
424
     * @return string the returned HTTP content
425
426
      public static function PostHttp($url, $postValues) {
427
      $options = [
428
      'http' => ['header' => 'Content-type: application/x-www-form-urlencoded\r\n', "method" => 'POST', 'content' => http_build_query($postValues)]
429
      ];
430
      $context = stream_context_create($options);
431
      return file_get_contents($url, false, $context);
432
      }
433
     * 
434
     */
435
}
436