Passed
Push — master ( 7dcdd9...287ee3 )
by Stefan
08:07 queued 17s
created

OutsideComm::adminInvitationMail()   F

Complexity

Conditions 19
Paths 363

Size

Total Lines 110
Code Lines 73

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 73
dl 0
loc 110
rs 1.6458
c 0
b 0
f 0
cc 19
nc 363
nop 6

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