Total Complexity | 60 |
Total Lines | 404 |
Duplicated Lines | 0 % |
Changes | 0 |
Complex classes like OutsideComm often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use OutsideComm, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
33 | class OutsideComm extends Entity |
||
34 | { |
||
35 | |||
36 | /** |
||
37 | * downloads a file from the internet |
||
38 | * @param string $url the URL to download |
||
39 | * @param int $timeout the timeout to download |
||
40 | * @return string|boolean the data we got back, or FALSE on failure |
||
41 | */ |
||
42 | public static function downloadFile($url, $timeout=0) |
||
43 | { |
||
44 | $loggerInstance = new \core\common\Logging(); |
||
45 | if (!preg_match("/:\/\//", $url)) { |
||
46 | $loggerInstance->debug(3, "The specified string does not seem to be a URL!"); |
||
47 | return FALSE; |
||
48 | } |
||
49 | # we got a URL, download it |
||
50 | if ($timeout > 0) { |
||
51 | $download = @fopen($url, 'rb', false, stream_context_create(['http' => ['timeout' => $timeout]])); |
||
52 | } else { |
||
53 | $download = fopen($url, "rb"); |
||
54 | } |
||
55 | if ($download === FALSE) { |
||
56 | $loggerInstance->debug(2, "Failed to open handle for $url \n"); |
||
57 | return FALSE; |
||
58 | } |
||
59 | $data = stream_get_contents($download); |
||
60 | if ($data === FALSE) { |
||
61 | $loggerInstance->debug(2, "Failed to download the file from $url"); |
||
62 | return FALSE; |
||
63 | } |
||
64 | return $data; |
||
65 | } |
||
66 | |||
67 | /** |
||
68 | * create an email handle from PHPMailer for later customisation and sending |
||
69 | * @return \PHPMailer\PHPMailer\PHPMailer |
||
70 | */ |
||
71 | public static function mailHandle() |
||
72 | { |
||
73 | // use PHPMailer to send the mail |
||
74 | $mail = new \PHPMailer\PHPMailer\PHPMailer(); |
||
75 | $mail->isSMTP(); |
||
76 | $mail->Port = 587; |
||
77 | $mail->SMTPSecure = 'tls'; |
||
78 | $mail->Host = \config\Master::MAILSETTINGS['host']; |
||
79 | if (\config\Master::MAILSETTINGS['user'] === NULL && \config\Master::MAILSETTINGS['pass'] === NULL) { |
||
80 | $mail->SMTPAuth = false; |
||
81 | } else { |
||
82 | $mail->SMTPAuth = true; |
||
83 | $mail->Username = \config\Master::MAILSETTINGS['user']; |
||
84 | $mail->Password = \config\Master::MAILSETTINGS['pass']; |
||
85 | } |
||
86 | $mail->SMTPOptions = \config\Master::MAILSETTINGS['options']; |
||
87 | // formatting nitty-gritty |
||
88 | $mail->WordWrap = 72; |
||
89 | $mail->isHTML(FALSE); |
||
90 | $mail->CharSet = 'UTF-8'; |
||
91 | $configuredFrom = \config\Master::APPEARANCE['from-mail']; |
||
92 | $mail->From = $configuredFrom; |
||
93 | // are we fancy? i.e. S/MIME signing? |
||
94 | if (isset(\config\Master::MAILSETTINGS['certfilename'], \config\Master::MAILSETTINGS['keyfilename'], \config\Master::MAILSETTINGS['keypass'])) { |
||
95 | $mail->sign(\config\Master::MAILSETTINGS['certfilename'], \config\Master::MAILSETTINGS['keyfilename'], \config\Master::MAILSETTINGS['keypass']); |
||
96 | } |
||
97 | return $mail; |
||
98 | } |
||
99 | |||
100 | const MAILDOMAIN_INVALID = -1000; |
||
101 | const MAILDOMAIN_NO_MX = -1001; |
||
102 | const MAILDOMAIN_NO_HOST = -1002; |
||
103 | const MAILDOMAIN_NO_CONNECT = -1003; |
||
104 | const MAILDOMAIN_NO_STARTTLS = 1; |
||
105 | const MAILDOMAIN_STARTTLS = 2; |
||
106 | |||
107 | /** |
||
108 | * verifies whether a mail address is in an existing and STARTTLS enabled mail domain |
||
109 | * |
||
110 | * @param string $address the mail address to check |
||
111 | * @return int status of the mail domain |
||
112 | */ |
||
113 | public static function mailAddressValidSecure($address) |
||
114 | { |
||
115 | $loggerInstance = new \core\common\Logging(); |
||
116 | if (!filter_var($address, FILTER_VALIDATE_EMAIL)) { |
||
117 | $loggerInstance->debug(4, "OutsideComm::mailAddressValidSecure: invalid mail address."); |
||
118 | return OutsideComm::MAILDOMAIN_INVALID; |
||
119 | } |
||
120 | $domain = substr($address, strpos($address, '@') + 1); // everything after the @ sign |
||
121 | // we can be sure that the @ was found (FILTER_VALIDATE_EMAIL succeeded) |
||
122 | // but let's be explicit |
||
123 | if ($domain === FALSE) { |
||
124 | return OutsideComm::MAILDOMAIN_INVALID; |
||
125 | } |
||
126 | // does the domain have MX records? |
||
127 | $mx = dns_get_record($domain, DNS_MX); |
||
128 | if ($mx === FALSE) { |
||
129 | $loggerInstance->debug(4, "OutsideComm::mailAddressValidSecure: no MX."); |
||
130 | return OutsideComm::MAILDOMAIN_NO_MX; |
||
131 | } |
||
132 | $loggerInstance->debug(5, "Domain: $domain MX: " . /** @scrutinizer ignore-type */ print_r($mx, TRUE)); |
||
133 | // create a pool of A and AAAA records for all the MXes |
||
134 | $ipAddrs = []; |
||
135 | foreach ($mx as $onemx) { |
||
136 | $v4list = dns_get_record($onemx['target'], DNS_A); |
||
137 | $v6list = dns_get_record($onemx['target'], DNS_AAAA); |
||
138 | foreach ($v4list as $oneipv4) { |
||
139 | $ipAddrs[] = $oneipv4['ip']; |
||
140 | } |
||
141 | foreach ($v6list as $oneipv6) { |
||
142 | $ipAddrs[] = "[" . $oneipv6['ipv6'] . "]"; |
||
143 | } |
||
144 | } |
||
145 | if (count($ipAddrs) == 0) { |
||
146 | $loggerInstance->debug(4, "OutsideComm::mailAddressValidSecure: no mailserver hosts."); |
||
147 | return OutsideComm::MAILDOMAIN_NO_HOST; |
||
148 | } |
||
149 | $loggerInstance->debug(5, "Domain: $domain Addrs: " . /** @scrutinizer ignore-type */ print_r($ipAddrs, TRUE)); |
||
150 | // connect to all hosts. If all can't connect, return MAILDOMAIN_NO_CONNECT. |
||
151 | // If at least one does not support STARTTLS or one of the hosts doesn't connect |
||
152 | // , return MAILDOMAIN_NO_STARTTLS (one which we can't connect to we also |
||
153 | // can't verify if it's doing STARTTLS, so better safe than sorry. |
||
154 | $retval = OutsideComm::MAILDOMAIN_NO_CONNECT; |
||
155 | $allWithStarttls = TRUE; |
||
156 | foreach ($ipAddrs as $oneip) { |
||
157 | $loggerInstance->debug(5, "OutsideComm::mailAddressValidSecure: connecting to $oneip."); |
||
158 | $smtp = new \PHPMailer\PHPMailer\SMTP; |
||
159 | if ($smtp->connect($oneip, 25)) { |
||
160 | // host reached! so at least it's not a NO_CONNECT |
||
161 | $loggerInstance->debug(4, "OutsideComm::mailAddressValidSecure: connected to $oneip."); |
||
162 | $retval = OutsideComm::MAILDOMAIN_NO_STARTTLS; |
||
163 | if ($smtp->hello('eduroam.org')) { |
||
164 | $extensions = $smtp->getServerExtList(); // Scrutinzer is wrong; is not always null - contains server capabilities |
||
165 | if (!is_array($extensions) || !array_key_exists('STARTTLS', $extensions)) { |
||
166 | $loggerInstance->debug(4, "OutsideComm::mailAddressValidSecure: no indication for STARTTLS."); |
||
167 | $allWithStarttls = FALSE; |
||
168 | } |
||
169 | } |
||
170 | } else { |
||
171 | // no connect: then we can't claim all targets have STARTTLS |
||
172 | $allWithStarttls = FALSE; |
||
173 | $loggerInstance->debug(5, "OutsideComm::mailAddressValidSecure: failed $oneip."); |
||
174 | } |
||
175 | } |
||
176 | // did the state $allWithStarttls survive? Then up the response to |
||
177 | // appropriate level. |
||
178 | if ($retval == OutsideComm::MAILDOMAIN_NO_STARTTLS && $allWithStarttls) { |
||
179 | $retval = OutsideComm::MAILDOMAIN_STARTTLS; |
||
180 | } |
||
181 | return $retval; |
||
182 | } |
||
183 | |||
184 | const SMS_SENT = 100; |
||
185 | const SMS_NOTSENT = 101; |
||
186 | const SMS_FRAGEMENTSLOST = 102; |
||
187 | |||
188 | /** |
||
189 | * Send SMS invitations to end users |
||
190 | * |
||
191 | * @param string $number the number to send to: with country prefix, but without the + sign ("352123456" for a Luxembourg example) |
||
192 | * @param string $content the text to send |
||
193 | * @return integer status of the sending process |
||
194 | * @throws \Exception |
||
195 | */ |
||
196 | public static function sendSMS($number, $content) |
||
250 | } |
||
251 | } |
||
252 | |||
253 | const INVITE_CONTEXTS = [ |
||
254 | 0 => "CO-ADMIN", |
||
255 | 1 => "NEW-FED", |
||
256 | 2 => "EXISTING-FED", |
||
257 | ]; |
||
258 | |||
259 | /** |
||
260 | * |
||
261 | * @param string $targets one or more mail addresses, comma-separated |
||
262 | * @param string $introtext introductory sentence (varies by situation) |
||
263 | * @param string $newtoken the token to send |
||
264 | * @param string $idpPrettyName the name of the IdP, in best-match language |
||
265 | * @param \core\Federation $federation if not NULL, indicates that invitation comes from authorised fed admin of that federation |
||
266 | * @param string $type the type of participant we're invited to |
||
267 | * @return array |
||
268 | * @throws \Exception |
||
269 | */ |
||
270 | public static function adminInvitationMail($targets, $introtext, $newtoken, $idpPrettyName, $federation, $type) |
||
271 | { |
||
272 | if (!in_array($introtext, OutsideComm::INVITE_CONTEXTS)) { |
||
273 | throw new \Exception("Unknown invite mode!"); |
||
274 | } |
||
275 | if ($introtext == OutsideComm::INVITE_CONTEXTS[1] && $federation === NULL) { // comes from fed admin, so federation must be set |
||
276 | throw new \Exception("Invitation from a fed admin, but we do not know the corresponding federation!"); |
||
277 | } |
||
278 | $prettyPrintType = ""; |
||
279 | switch ($type) { |
||
280 | case \core\IdP::TYPE_IDP: |
||
281 | $prettyPrintType = Entity::$nomenclature_idp; |
||
282 | break; |
||
283 | case \core\IdP::TYPE_SP: |
||
284 | $prettyPrintType = Entity::$nomenclature_hotspot; |
||
285 | break; |
||
286 | case \core\IdP::TYPE_IDPSP: |
||
287 | $prettyPrintType = sprintf(_("%s and %s"), Entity::$nomenclature_idp, Entity::$nomenclature_hotspot); |
||
288 | break; |
||
289 | default: |
||
290 | throw new \Exception("This is controlled vocabulary, impossible."); |
||
291 | } |
||
292 | Entity::intoThePotatoes(); |
||
293 | $mail = OutsideComm::mailHandle(); |
||
294 | new \core\CAT(); // makes sure Entity is initialised |
||
295 | // we have a few stock intro texts on file |
||
296 | $introTexts = [ |
||
297 | OutsideComm::INVITE_CONTEXTS[0] => sprintf(_("a %s of the %s %s \"%s\" has invited you to manage the %s together with them."), Entity::$nomenclature_fed, \config\ConfAssistant::CONSORTIUM['display_name'], Entity::$nomenclature_participant, $idpPrettyName, Entity::$nomenclature_participant), |
||
298 | 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), |
||
299 | 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), |
||
300 | ]; |
||
301 | $validity = sprintf(_("This invitation is valid for 24 hours from now, i.e. until %s."), strftime("%x %X %Z", time() + 86400)); |
||
302 | // need some nomenclature |
||
303 | // are we on https? |
||
304 | $proto = "http://"; |
||
305 | if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == "on") { |
||
306 | $proto = "https://"; |
||
307 | } |
||
308 | // then, send out the mail |
||
309 | $message = _("Hello,") . "\n\n" . wordwrap($introTexts[$introtext] . " " . $validity, 72) . "\n\n"; |
||
310 | // default means we don't have a Reply-To. |
||
311 | $replyToMessage = wordwrap(_("manually. Please do not reply to this mail; this is a send-only address.")); |
||
312 | |||
313 | if ($federation !== NULL) { |
||
314 | // see if we are supposed to add a custom message |
||
315 | $customtext = $federation->getAttributes('fed:custominvite'); |
||
316 | if (count($customtext) > 0) { |
||
317 | $message .= wordwrap(sprintf(_("Additional message from your %s administrator:"), Entity::$nomenclature_fed), 72) . "\n---------------------------------" . |
||
318 | wordwrap($customtext[0]['value'], 72) . "\n---------------------------------\n\n"; |
||
319 | } |
||
320 | // and add Reply-To already now |
||
321 | foreach ($federation->listFederationAdmins() as $fedadmin_id) { |
||
322 | $fedadmin = new \core\User($fedadmin_id); |
||
323 | $mailaddrAttrib = $fedadmin->getAttributes("user:email"); |
||
324 | $nameAttrib = $fedadmin->getAttributes("user:realname"); |
||
325 | $name = $nameAttrib[0]['value'] ?? sprintf(_("%s administrator"), Entity::$nomenclature_fed); |
||
326 | if (count($mailaddrAttrib) > 0) { |
||
327 | $mail->addReplyTo($mailaddrAttrib[0]['value'], $name); |
||
328 | $replyToMessage = wordwrap(sprintf(_("manually. If you reply to this mail, you will reach your %s administrators."), Entity::$nomenclature_fed), 72); |
||
329 | } |
||
330 | } |
||
331 | } |
||
332 | $productname = \config\Master::APPEARANCE['productname']; |
||
333 | $consortium = \config\ConfAssistant::CONSORTIUM['display_name']; |
||
334 | $message .= wordwrap(sprintf(_("To enlist as an administrator for that %s, please click on the following link:"), Entity::$nomenclature_participant), 72) . "\n\n" . |
||
335 | $proto . $_SERVER['SERVER_NAME'] . \config\Master::PATHS['cat_base_url'] . "admin/action_enrollment.php?token=$newtoken\n\n" . |
||
336 | wordwrap(sprintf(_("If clicking the link doesn't work, you can also go to the %s Administrator Interface at"), $productname), 72) . "\n\n" . |
||
337 | $proto . $_SERVER['SERVER_NAME'] . \config\Master::PATHS['cat_base_url'] . "admin/\n\n" . |
||
338 | _("and enter the invitation token") . "\n\n" . |
||
339 | $newtoken . "\n\n$replyToMessage\n\n" . |
||
340 | 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" . |
||
341 | wordwrap(sprintf(_("We wish you a lot of fun with the %s."), $productname), 72) . "\n\n" . |
||
342 | sprintf(_("Sincerely,\n\nYour friendly folks from %s Operations"), $consortium); |
||
343 | |||
344 | |||
345 | // who to whom? |
||
346 | $mail->FromName = \config\Master::APPEARANCE['productname'] . " Invitation System"; |
||
347 | |||
348 | if (isset(\config\Master::APPEARANCE['invitation-bcc-mail']) && \config\Master::APPEARANCE['invitation-bcc-mail'] !== NULL) { |
||
349 | $mail->addBCC(\config\Master::APPEARANCE['invitation-bcc-mail']); |
||
350 | } |
||
351 | |||
352 | // all addresses are wrapped in a string, but PHPMailer needs a structured list of addressees |
||
353 | // sigh... so convert as needed |
||
354 | // first split multiple into one if needed |
||
355 | $recipients = explode(", ", $targets); |
||
356 | |||
357 | $secStatus = TRUE; |
||
358 | $domainStatus = TRUE; |
||
359 | |||
360 | // fill the destinations in PHPMailer API |
||
361 | foreach ($recipients as $recipient) { |
||
362 | $mail->addAddress($recipient); |
||
363 | $status = OutsideComm::mailAddressValidSecure($recipient); |
||
364 | if ($status < OutsideComm::MAILDOMAIN_STARTTLS) { |
||
365 | $secStatus = FALSE; |
||
366 | } |
||
367 | if ($status < 0) { |
||
368 | $domainStatus = FALSE; |
||
369 | } |
||
370 | } |
||
371 | Entity::outOfThePotatoes(); |
||
372 | if (!$domainStatus) { |
||
373 | return ["SENT" => FALSE, "TRANSPORT" => FALSE]; |
||
374 | } |
||
375 | |||
376 | // what do we want to say? |
||
377 | Entity::intoThePotatoes(); |
||
378 | $mail->Subject = sprintf(_("%s: you have been invited to manage an %s"), \config\Master::APPEARANCE['productname'], Entity::$nomenclature_participant); |
||
379 | Entity::outOfThePotatoes(); |
||
380 | $mail->Body = $message; |
||
381 | return ["SENT" => $mail->send(), "TRANSPORT" => $secStatus]; |
||
382 | } |
||
383 | |||
384 | /** |
||
385 | * sends a POST with some JSON inside |
||
386 | * |
||
387 | * @param string $url the URL to POST to |
||
388 | * @param array $dataArray the data to be sent in PHP array representation |
||
389 | * @return array the JSON response, decoded into PHP associative array |
||
390 | * @throws \Exception |
||
391 | */ |
||
392 | public static function postJson($url, $dataArray) |
||
411 | } |
||
412 | |||
413 | /** |
||
414 | * aborts code execution if a required mail address is invalid |
||
415 | * |
||
416 | * @param mixed $newmailaddress input string, possibly one or more mail addresses |
||
417 | * @return array mail addresses that passed validation |
||
418 | */ |
||
419 | public static function exfiltrateValidAddresses($newmailaddress) |
||
437 | } |
||
438 | /** |
||
439 | * performs an HTTP request. Currently unused, will be for external CA API calls. |
||
440 | * |
||
455 |