core/diag/Logopath.php

Summary

Maintainability
D
2 days
Test Coverage
<?php

/*
 * *****************************************************************************
 * Contributions to this work were made on behalf of the GÉANT project, a 
 * project that has received funding from the European Union’s Framework 
 * Programme 7 under Grant Agreements No. 238875 (GN3) and No. 605243 (GN3plus),
 * Horizon 2020 research and innovation programme under Grant Agreements No. 
 * 691567 (GN4-1) and No. 731122 (GN4-2).
 * On behalf of the aforementioned projects, GEANT Association is the sole owner
 * of the copyright in all material which was developed by a member of the GÉANT
 * project. GÉANT Vereniging (Association) is registered with the Chamber of 
 * Commerce in Amsterdam with registration number 40535155 and operates in the 
 * UK as a branch of GÉANT Vereniging.
 * 
 * Registered office: Hoekenrode 3, 1102BR Amsterdam, The Netherlands. 
 * UK branch address: City House, 126-130 Hills Road, Cambridge CB2 1PQ, UK
 *
 * License: see the web/copyright.inc.php file in the file structure or
 *          <base_url>/copyright.php after deploying the software
 */

namespace core\diag;

/**
 * This class evaluates the evidence of previous Telepath and/or Sociopath runs
 * and figures out whom to send emails to, and with that content. It then sends
 * these emails.
 */
class Logopath extends AbstractTest {

    /**
     * storing the end user's email, if he has given it to us
     * @var string|boolean
     */
    private $userEmail;

    /**
     * maybe the user has some additional evidence directly on his device?
     * @var string|boolean
     */
    private $additionalScreenshot;

    /**
     * the list of mails to send
     * @var array
     */
    private $mailStack;

    /*
     * categories of people to contact
     */

    const TARGET_EDUROAM_OT = 0;
    const TARGET_NRO_IDP = 1;
    const TARGET_NRO_SP = 2;
    const TARGET_IDP = 3;
    const TARGET_SP = 4;
    const TARGET_ENDUSER = 5;

    /** we start all our mails with a common prefix, internationalised
     *
     * @var string
     */
    private $subjectPrefix;

    /** and we end with a greeting/disclaimer
     *
     * @var string
     */
    private $finalGreeting;

    /**
     * We need to vet user inputs.
     * @var \web\lib\common\InputValidation
     */
    private $validatorInstance;

    /**
     * will be filled with the exact emails to send, by determineMailsToSend()
     * @var array
     */
    private $mailQueue;

    /**
     *
     * @var array
     */
    private $concreteRecipients;

    /*
     *  cases to consider
     */

    const IDP_EXISTS_BUT_NO_DATABASE = 100;
    const IDP_SUSPECTED_PROBLEM_INTERACTIVE_FORCED = 101;
    const IDP_SUSPECTED_PROBLEM_INTERACTIVE_EVIDENCED = 102;

    /*
     * types of supplemental string data to send 
     */

    /**
     * initialise the class: maintain state of existing evidence, and get translated versions of email texts etc.
     */
    public function __construct() {
        parent::__construct();
        \core\common\Entity::intoThePotatoes();
        $this->userEmail = FALSE;
        $this->additionalScreenshot = FALSE;

        $this->mailQueue = [];
        $this->concreteRecipients = [];

        $this->validatorInstance = new \web\lib\common\InputValidation();

        $this->possibleFailureReasons = $_SESSION["SUSPECTS"] ?? []; // if we know nothing, don't talk to anyone
        $this->additionalFindings = $_SESSION["EVIDENCE"] ?? [];

        $this->subjectPrefix = _("[eduroam Diagnostics]") . " ";
        $this->finalGreeting = "\n"
                . _("(This service is in an early stage. We apologise if this is a false alert. If this is the case, please send an email report to cat-devel@lists.geant.org, forwarding the entire message (including the 'SUSPECTS' and 'EVIDENCE' data at the end), and explain why this is a false positive.)")
                . "\n"
                . _("Yours sincerely,") . "\n"
                . "\n"
                . _("Ed U. Roam, the eduroam diagnostics algorithm");

        $this->mailStack = [
            Logopath::IDP_EXISTS_BUT_NO_DATABASE => [
                "to" => [Logopath::TARGET_NRO_IDP],
                "cc" => [Logopath::TARGET_EDUROAM_OT],
                "bcc" => [],
                "reply-to" => [Logopath::TARGET_EDUROAM_OT],
                "subject" => _("[POLICYVIOLATION NATIONAL] IdP with no entry in eduroam database"),
                "body" => _("Dear NRO administrator,") . "\n"
                . "\n"
                . wordwrap(sprintf(_("an end-user requested diagnostics for realm %s. Real-time connectivity checks determined that the realm exists, but we were unable to find an IdP with that realm in the eduroam database."), $this->additionalFindings['REALM'])) . "\n"
                . "\n"
                . _("By not listing IdPs in the eduroam database, you are violating the eduroam policy.") . "\n"
                . "\n"
                . _("Additionally, this creates operational issues. In particular, we are unable to direct end users to their IdP for further diagnosis/instructions because there are no contact points for that IdP in the database.") . "\n"
                . "\n"
                . _("Please stop the policy violation ASAP by listing the IdP which is associated to this realm.")
                . "\n",
            ],
            Logopath::IDP_SUSPECTED_PROBLEM_INTERACTIVE_FORCED => [
                "to" => [Logopath::TARGET_IDP],
                "cc" => [],
                "bcc" => [],
                "reply-to" => [Logopath::TARGET_ENDUSER],
                "subject" => _("[TECHNICAL PROBLEM] Administrator suspects technical problem with your IdP"),
                "body" => _("Dear IdP administrator,") . "\n"
                . "\n"
                . sprintf(_("an organisation administrator requested diagnostics for realm %s. "), $this->additionalFindings['REALM'])
                . "\n"
                . _("Real-time connectivity checks determined that the realm appears to be working in acceptable parameters, but the administrator insisted to contact you with the supplemental information below.") . "\n"
                . "\n",
            ],
            Logopath::IDP_SUSPECTED_PROBLEM_INTERACTIVE_EVIDENCED => [
                "to" => [Logopath::TARGET_IDP],
                "cc" => [],
                "bcc" => [],
                "reply-to" => [Logopath::TARGET_ENDUSER],
                "subject" => _("[TECHNICAL PROBLEM] Administrator suspects technical problem with your IdP"),
                "body" => _("Dear IdP administrator,") . "\n"
                . "\n"
                . sprintf(_("an organisation administrator requested diagnostics for realm %s. "), $this->additionalFindings['REALM'])
                . "\n"
                . _("Real-time connectivity checks determined that the realm indeed has an operational problem at this point in time. Please see the supplemental information below.") . "\n"
                . "\n",
            ],
        ];

        // add exalted human-readable information to main mail body
        foreach ($this->mailStack as $oneEntry) {
            if (isset($this->additionalFindings['INTERACTIVE_ENDUSER_AUTH_TIMESTAMP'])) {
                $oneEntry["body"] .= _("Authentication/Attempt Timestamp of user session:") . " " . $this->additionalFindings['INTERACTIVE_ENDUSER_AUTH_TIMESTAMP'] . "\n";
            }
            if (isset($this->additionalFindings['INTERACTIVE_ENDUSER_MAC'])) {
                $oneEntry["body"] .= _("MAC address of end user in question:") . " " . $this->additionalFindings['INTERACTIVE_ENDUSER_MAC'] . "\n";
            }
            if (isset($this->additionalFindings['INTERACTIVE_ADDITIONAL_COMMENTS'])) {
                $oneEntry["body"] .= _("Additional Comments:") . " " . $this->additionalFindings['INTERACTIVE_ADDITIONAL_COMMENTS'] . "\n";
            }
        }

        \core\common\Entity::outOfThePotatoes();
    }

    /**
     * if the system asked the user for his email and he's willing to give it to
     * us, store it with this function
     * 
     * @param string $userEmail the end-users email to store
     * @return void
     */
    public function addUserEmail($userEmail) {
// returns FALSE if it was not given or bogus, otherwise stores this as mail target
        $this->userEmail = $this->validatorInstance->email($userEmail);
    }

    /**
     * if the system asked the user for a screenshot and he's willing to give one
     * to us, store it with this function
     * 
     * @param string $binaryData the submitted binary data, to be vetted
     * @return void
     */
    public function addScreenshot($binaryData) {
        if ($this->validatorInstance->image($binaryData) === TRUE) {
            if (class_exists('\\Gmagick')) { 
                $magick = new \Gmagick(); 
            } else {
                $magick = new \Imagick();
            }
            $magick->readimageblob($binaryData);
            $magick->setimageformat("png");
            $this->additionalScreenshot = $magick->getimageblob();
        } else {
            // whatever we got, it didn't parse as an image
            $this->additionalScreenshot = FALSE;
        }
    }

    /**
     * looks at probabilities and evidence, and decides which mail scenario(s) to send
     * 
     * @return void
     */
    private function determineMailsToSend() {
        $this->mailQueue = [];
// check for IDP_EXISTS_BUT_NO_DATABASE
        if (!in_array(AbstractTest::INFRA_NONEXISTENTREALM, $this->possibleFailureReasons) && $this->additionalFindings[AbstractTest::INFRA_NONEXISTENTREALM]['DATABASE_STATUS']['ID2'] < 0) {
            $this->mailQueue[] = Logopath::IDP_EXISTS_BUT_NO_DATABASE;
        }

        if (in_array(AbstractTest::INFRA_IDP_ADMIN_DETERMINED_EVIDENCED, $this->possibleFailureReasons)) {
            $this->mailQueue[] = Logopath::IDP_SUSPECTED_PROBLEM_INTERACTIVE_EVIDENCED;
        }
        if (in_array(AbstractTest::INFRA_IDP_ADMIN_DETERMINED_FORCED, $this->possibleFailureReasons)) {
            $this->mailQueue[] = Logopath::IDP_SUSPECTED_PROBLEM_INTERACTIVE_FORCED;
        }

// after collecting all the conditions, find the target entities in all
// the mails, and check if they resolve to a known mail address. If they
// do not, this triggers more mails about missing contact info.

        $abstractRecipients = [];
        foreach ($this->mailQueue as $mail) {
            $abstractRecipients = array_unique(array_merge($this->mailStack[$mail]['to'], $this->mailStack[$mail]['cc'], $this->mailStack[$mail]['bcc'], $this->mailStack[$mail]['reply-to']));
        }
// who are those guys? Here is significant legwork in terms of DB lookup
        $this->concreteRecipients = [];
        foreach ($abstractRecipients as $oneRecipient) {
            switch ($oneRecipient) {
                case Logopath::TARGET_EDUROAM_OT:
                    $this->concreteRecipients[Logopath::TARGET_EDUROAM_OT] = ["eduroam-ot@lists.geant.org"];
                    break;
                case Logopath::TARGET_ENDUSER:
// will be filled when sending, from $this->userEmail
// hence the +1 below
                    break;
                case Logopath::TARGET_IDP:
                    // CAT contacts, if existing
                    if ($this->additionalFindings['INFRA_NONEXISTENT_REALM']['DATABASE_STATUS']['ID1'] > 0) {
                        $profile = \core\ProfileFactory::instantiate($this->additionalFindings['INFRA_NONEXISTENT_REALM']['DATABASE_STATUS']['ID1']);

                        foreach ($profile->getAttributes("support:email") as $oneMailAddress) {
                            // CAT contacts are always public
                            $this->concreteRecipients[Logopath::TARGET_IDP][] = $oneMailAddress;
                        }
                    }
                    // DB contacts, if existing
                    if ($this->additionalFindings['INFRA_NONEXISTENT_REALM']['DATABASE_STATUS']['ID2'] > 0) {
                        $cat = new \core\CAT();
                        $info = $cat->getExternalDBEntityDetails($this->additionalFindings['INFRA_NONEXISTENT_REALM']['DATABASE_STATUS']['ID2']);
                        foreach ($info['admins'] as $infoElement) {
                            if (isset($infoElement['email'])) {
                                // until DB Spec 2.0 is out and used, consider all DB contacts as private
                                $this->concreteRecipients[Logopath::TARGET_IDP][] = $infoElement['email'];
                            }
                        }
                    }
                    break;
                case Logopath::TARGET_NRO_IDP: // same code for both, fall through
                case Logopath::TARGET_NRO_SP:
                    $target = ($oneRecipient == Logopath::TARGET_NRO_IDP ? $this->additionalFindings['INFRA_NRO_IdP'] : $this->additionalFindings['INFRA_NRO_SP']);
                    $fed = new \core\Federation($target);
                    $adminList = $fed->listFederationAdmins();
                    // TODO: we only have those who are signed up for CAT currently, and by their ePTID.
                    // in touch with OT to get all, so that we can get a list of emails
                    break;
                case Logopath::TARGET_SP:
                    // TODO: needs a DB view on SPs in eduroam DB, in touch with OT
                    break;
            }
        }
// now see if we lack pertinent recipient info, and add corresponding
// mails to the list
        if (count($abstractRecipients) != count($this->concreteRecipients) + 1) {
            // there is a discrepancy, do something ...
            // we need to add a mail to the next higher hierarchy level as escalation
            // but may also have to remove the lower one because we don't know the guy.
        }
    }

    /**
     * sees if it is useful to ask the user for his contact details or screenshots
     * @return boolean
     */
    public function isEndUserContactUseful() {
        $contactUseful = FALSE;
        $this->determineMailsToSend();
        foreach ($this->mailQueue as $oneMail) {
            if (in_array(Logopath::TARGET_ENDUSER, $this->mailStack[$oneMail]['to']) ||
                    in_array(Logopath::TARGET_ENDUSER, $this->mailStack[$oneMail]['cc']) ||
                    in_array(Logopath::TARGET_ENDUSER, $this->mailStack[$oneMail]['bcc']) ||
                    in_array(Logopath::TARGET_ENDUSER, $this->mailStack[$oneMail]['reply-to'])) {
                $contactUseful = TRUE;
            }
        }
        return $contactUseful;
    }

    const CATEGORYBINDING = ['to' => 'addAddress', 'cc' => 'addCC', 'bcc' => 'addBCC', 'reply-to' => 'addReplyTo'];

    /**
     * sends the mails. Only call this after either determineMailsToSend() or
     * isEndUserContactUseful(), otherwise it will do nothing.
     * 
     * @return void
     */
    public function weNeedToTalk() {
        $this->determineMailsToSend();
        foreach ($this->mailQueue as $oneMail) {
            $theMail = $this->mailStack[$oneMail];
            // if user interaction would have been good, but the user didn't 
            // leave his mail address, remove him/her from the list of recipients
            foreach (Logopath::CATEGORYBINDING as $index => $functionName) {
                if (in_array(Logopath::TARGET_ENDUSER, $theMail[$index]) && $this->userEmail === FALSE) {
                    $theMail[$index] = array_diff($theMail[$index], [Logopath::TARGET_ENDUSER]);
                }
            }

            $handle = \core\common\OutsideComm::mailHandle();
            // let's identify ourselves
            $handle->FromName = \config\Master::APPEARANCE['productname'] . " Real-Time Diagnostics System";
            // add recipients
            foreach (Logopath::CATEGORYBINDING as $arrayName => $functionName) {
                foreach ($theMail[$arrayName] as $onePrincipal) {
                    foreach ($this->concreteRecipients[$onePrincipal] as $oneConcrete) {
                        $handle->{$functionName}($oneConcrete);
                    }
                }
            }
            // and add what to say
            $handle->Subject = $theMail['subject'];
            $handle->Body = $theMail['body'];
            if (is_string($this->additionalScreenshot)) {
                $handle->addStringAttachment($this->additionalScreenshot, "screenshot.png", "base64", "image/png", "attachment");
            }
            $handle->send();
        }
    }

}