<?PHP
#
#   FILE:  Captcha.php
#
#   Part of the Collection Workflow Integration System (CWIS)
#   Copyright 2002-2013 Edward Almasy and Internet Scout Research Group
#   http://scout.wisc.edu/cwis/
#

class Captcha extends Plugin
{
    /**
    * Register the Captcha plugin
    */
    public function Register()
    {
        $this->Name = "CAPTCHA Anti-Spam Support";
        $this->Version = "1.0.8";
        $this->Description = "Adds <a href=\"http://captcha.net\" "
            ."target=\"_blank\">CAPTCHA</a> "
            ."support to protect against attacks by spammers using bots. ";
        $this->Author = "Internet Scout";
        $this->Url = "http://scout.wisc.edu/cwis/";
        $this->Email = "scout@scout.wisc.edu";
        $this->Requires = array("CWISCore" => "2.0.3");
        $this->CfgSetup["Method"] =
          array(
                "Type" => "Option",
                "Label" => "CAPTCHA Method",
                "Help" =>
                  "Select what manner of CAPTCHA you wish to display.",
                "Options" =>
                array("Securimage" => "SecurImage CAPTCHA")
                );
        $this->CfgSetup["Width"] =
          array( "Type" => "Number",
                 "MaxVal" => 1024,
                 "Label" => "Width",
                 "Help" => "Width of the captcha image");
        $this->CfgSetup["Height"] =
          array( "Type" => "Number",
                 "MaxVal" => 1024,
                 "Label" => "Height",
                 "Help" => "Height of the captcha image");
        $this->CfgSetup["DisplayIfLoggedIn"] =
            array(
                "Type" => "Flag",
                "Label" => "Display CAPTCHA for logged-in users",
                "Help" => "",
                "OnLabel" => "Yes",
                "OffLabel" => "No",
                );
        $this->CfgSetup["CaptchaCommentPost"] =
            array(
                "Type" => "Flag",
                "Label" => "Display CAPTCHA for comment posting",
                "Help" => "",
                "OnLabel" => "Yes",
                "OffLabel" => "No",
                );
        $this->CfgSetup["CaptchaMessagePost"] =
            array(
                "Type" => "Flag",
                "Label" => "Display CAPTCHA for message posting",
                "Help" => "",
                "OnLabel" => "Yes",
                "OffLabel" => "No",
                );
        $this->CfgSetup["CaptchaFeedback"] =
            array(
                "Type" => "Flag",
                "Label" => "Display CAPTCHA for sending feedback",
                "Help" => "",
                "OnLabel" => "Yes",
                "OffLabel" => "No",
                );
        $this->CfgSetup["CaptchaSignup"] =
            array(
                "Type" => "Flag",
                "Label" => "Display CAPTCHA for account signup",
                "Help" => "",
                "OnLabel" => "Yes",
                "OffLabel" => "No",
                );
    }

    /**
    * Initialize the plugin.
    */
    public function Initialize()
    {
        $this->AddToIncludePath();
    }

    /**
    * Install the Captcha plugin.
    */
    public function Install()
    {
        $DB = new Database();

        # make the installation process fault tolerant
        $DB->SetQueryErrorsToIgnore(array(
            '/CREATE\s+TABLE\s+[^\s]+\s+\([^)]+\)/i'
              => '/Table\s+[^\s]+\s+already\s+exists/i'));

        # create IP log table
        if (FALSE === $DB->Query("
            CREATE TABLE Captcha_IpLog (
                ClientIp VARCHAR(15) UNIQUE,
                Views INT,
                Successes INT,
                Failures INT,
                LastSeen TIMESTAMP
            );"))
        { return "Could not create the IP log table"; }

        # create user log table
        if (FALSE === $DB->Query("
            CREATE TABLE Captcha_UserLog (
                UserName VARCHAR(15) UNIQUE,
                Views INT,
                Successes INT,
                Failures INT,
                LastSeen TIMESTAMP
            );"))
        { return "Could not create the user log table"; }

        $this->ConfigSetting("Method", "Securimage");
        $this->ConfigSetting("Width", 115);
        $this->ConfigSetting("Height", 40);
        $this->ConfigSetting("DisplayIfLoggedIn", TRUE);
        $this->ConfigSetting("CaptchaCommentPost", TRUE);
        $this->ConfigSetting("CaptchaMessagePost", TRUE);
        $this->ConfigSetting("CaptchaFeedback", TRUE);
        $this->ConfigSetting("CaptchaSignup", TRUE);

        return NULL;
    }

    /**
    * Uninstall the plugin.
    * @return NULL|string : NULL if successful or an error message otherwise
    */
    public function Uninstall()
    {
        $DB = new Database();

        foreach( array("Captcha_IpLog" => "IP log",
                       "Captcha_UserLog" => "user log") as $Table => $Desc)
        {
            if (FALSE === $DB->Query("DROP TABLE ".$Table) )
            { return "Could not remove the ".$Desc." table"; }
        }
    }

    /**
    * Upgrade the Captcha plugin.
    * @param string $PreviousVersion Old version to upgrade from
    */
    public function Upgrade($PreviousVersion)
    {
        $DB = new Database();

        # ugprade from versions < 1.0.1 to 2.0.0
        if (version_compare($PreviousVersion, "1.0.1", "<"))
        {
            $Method = $DB->Query("
                SELECT V FROM CaptchaPrefs
                WHERE K='Method'", "Method");

            # migrate existing plugin configuration from the database
            $this->ConfigSetting("Method", $Method);

            # new image dimensions configuration
            $this->ConfigSetting("Width", 115);
            $this->ConfigSetting("Height", 40);
        }

        # upgrade from version 1.0.1 to 1.0.2
        if (version_compare($PreviousVersion, "1.0.2", "<"))
        {
            $Method = $this->ConfigSetting("Method");

            # disabling from the preferences is no longer an option since it's
            # assumed when enabling/disabling the plugin
            if (is_null($Method) || $Method == "None" || $Method == "Disabled")
            {
                $this->ConfigSetting("Method", "Securimage");
            }
        }

        if (version_compare($PreviousVersion, "1.0.3", "<"))
        {
            $this->ConfigSetting("CaptchaCommentPost", TRUE);
            $this->ConfigSetting("CaptchaMessagePost", TRUE);
            $this->ConfigSetting("CaptchaFeedback", TRUE);
            $this->ConfigSetting("CaptchaSignup", TRUE);
        }

        if (version_compare($PreviousVersion, "1.0.4", "<"))
        {
            $DB->Query("ALTER TABLE CaptchaIpLog "
                ."RENAME TO Captcha_IpLog");
            $DB->Query("ALTER TABLE CaptchaUserLog "
                ."RENAME TO Captcha_UserLog");
        }

        if (version_compare($PreviousVersion, "1.0.5", "<"))
        {
            $this->ConfigSetting("DisplayIfLoggedIn", TRUE);
        }
    }

    /**
    * Hook CAPTCAH plugin into the event system.
    * @return array Events to hook.
    */
    public function HookEvents()
    {
        return array(
            "EVENT_SYSTEM_ADMINISTRATION_MENU" => "SysAdminMenu",
            "EVENT_PAGE_LOAD" => "ManageState",
            "EVENT_APPEND_HTML_TO_FORM" => "AppendHtmlToForm",
            "EVENT_VALIDATE_USER_COMMENT" => "VerifyComment",
            "EVENT_USER_LOGIN" => "ResetState",
            "EVENT_USER_LOGOUT" => "ResetState",
            "EVENT_USER_SIGNUP_VERIFY" => "VerifySignup",
            "EVENT_USER_SIGNUP_EXTEND_ERROR_LIST" => "ExtendSignupErrorList",
            );
    }

    /**
    *  Add a View Captcha Logs entry to the system administration menu
    * @return Array of pages to add.
    */
    public function SysAdminMenu()
    {
        return array(
            "Log" => "View Captcha Logs",
            );
    }

    /**
    * Display a Captcha on forms per the Captcha plugin configuration.
    * @param string $PageName Name of the current page.
    * @param string $FormName Name of the form displayed.
    * @param array $Labels Array of labels for added elements.
    * @param array $InputElements Array of added input elements.
    * @param array $Notes Notes to display for each element.
    * @return updated versions of the input parameters.
    */
    public function AppendHtmlToForm($PageName, $FormName,
                $Labels, $InputElements, $Notes)
    {
        global $User;

        # check if the CAPTCHA should be displayed for logged in users
        if ($User->IsLoggedIn() && !$this->ConfigSetting("DisplayIfLoggedIn"))
        {
            return array("PageName"      => $PageName,
                         "FormName"      => $FormName,
                         "Labels"        => $Labels,
                         "InputElements" => $InputElements,
                         "Notes"         => $Notes);
        }

        if ($this->ConfigSetting("CaptchaCommentPost"))
        {
            $PlacesToDisplay["AddResourceComment"]["CommentForm"] = TRUE;
            $PlacesToDisplay["PreviewComment"]["CommentForm"] = TRUE;
        }

        if ($this->ConfigSetting("CaptchaMessagePost"))
        {
            $PlacesToDisplay["PostMessage"]["CommentForm"] = TRUE;
            $PlacesToDisplay["PreviewMessage"]["CommentForm"] = TRUE;
            $PlacesToDisplay["AddTopic"]["CommentForm"] = TRUE;
        }

        if ($this->ConfigSetting("CaptchaFeedback"))
        {
            $PlacesToDisplay["LeaveFeedback"]["FeedbackForm"] = TRUE;
        }

        if ($this->ConfigSetting("CaptchaSignup"))
        {
            $PlacesToDisplay["RequestAccount"]["NewAccountForm"] = TRUE;
        }

        if ($GLOBALS["G_PluginManager"]->PluginEnabled("MailingList"))
        {
            $PlacesToDisplay["P_MailingList_Subscribe"]["MailingList_Subscribe"] = TRUE;
        }

        if ( $this->AlreadySolved !== TRUE &&
             isset($PlacesToDisplay[$PageName][$FormName]) )
        {
            $MyUrl = './plugins/Captcha/';
            $UpdateDB = FALSE;
            $BaseUrl = $GLOBALS["AF"]->BaseUrl();

            $Method = $this->ConfigSetting("Method");
            if ($Method == "Securimage")
            {
                #- For the securimage method:
                # (the SID is not used by securimage_show, but is passed to prevent
                #  browsers from caching the content of that page).
                $Labels[]= "Verification Code:";

                // @codingStandardsIgnoreStart
                $InputElements[]=
                    '<table>'
                    .'<tr><td rowspan=2><img id="captcha" '
                    .'src="'.$BaseUrl.'index.php?P=P_Captcha_DisplayImage&sid='.md5(time()).'" '
                    .'width="'.$this->ConfigSetting("Width").'" height="'.$this->ConfigSetting("Height").'" '
                    .'alt="CAPTCHA Image" /></td>'
                    .'<td><object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" '
                    .'codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,0,0" '
                    .'width="19" height="19" id="SecurImage_as3" align="middle">'
                    .'<param name="allowScriptAccess" value="sameDomain" />'
                    .'<param name="allowFullScreen" value="false" />'
                    .'<param name="movie" value="securimage_play.swf?audio='
                    .$BaseUrl.'index.php&#63;P&#61;P_Captcha_Sound&bgColor1=#777'
                    .'&bgColor2=#fff&iconColor=#000&roundedCorner=5" />'
                    .'<param name="quality" value="high" />'
                    .'<param name="bgcolor" value="#ffffff" />'
                    .'<embed src="'.$MyUrl.'lib/securimage/securimage_play.swf?audio='
                    .$BaseUrl.'index.php&#63;P&#61;P_Captcha_Sound&bgColor1=#777'
                    .'&bgColor2=#fff&iconColor=#000&roundedCorner=5" quality="high" '
                    .'bgcolor="#ffffff" width="19" height="19" name="SecurImage_as3" '
                    .'align="middle" allowScriptAccess="sameDomain" allowFullScreen="false" '
                    .'type="application/x-shockwave-flash" '
                    .'pluginspage="http://www.macromedia.com/go/getflashplayer" />'
                    .'</object></td>'
                    .'</tr><tr>'
                    .'<td><a tabindex="-1" style="border-style: none" href="#" title="Refresh Image" '
                    .'onclick="document.getElementById(\'captcha\').src = \''
                    .$BaseUrl.'index.php?P=P_Captcha_DisplayImage&sid=\' + Math.random(); return false">'
                    .'<img src="'.$MyUrl.'lib/securimage/images/refresh.gif" alt="Reload Image" border="0" '
                    .'onclick="this.blur()" align="bottom" /></a></td>'
                    .'</tr></table><br>'
                    .'<input type="text" name="captcha_code" '
                    .'size="10" maxlength="6" />';
                // @codingStandardsIgnoreEnd

                $Notes[]=
                "Please enter the above code to verify that you're not a spam robot.";

                $UpdateDB = TRUE;
            }

            # Now that the CAPTCHA has been displayed, log a view
            # for this user/ip, creating DB entries where required
            # if this is the first time we've seen this user.
            if ($UpdateDB)
            {
                $DB = new Database();
                $DB->Query("INSERT IGNORE INTO "
                           ."Captcha_IpLog (ClientIp,Views,Successes,Failures) "
                           ."VALUES "
                           ."('".$_SERVER["REMOTE_ADDR"]."',0,0,0)");
                $DB->Query("UPDATE Captcha_IpLog "
                           ."SET Views=Views+1, LastSeen=NOW() "
                           ."WHERE ClientIp='".$_SERVER["REMOTE_ADDR"]."'");

                global $G_User;
                if ($G_User->IsLoggedIn())
                {
                    $DB->Query("INSERT IGNORE INTO "
                               ."Captcha_UserLog (UserName,Views,Successes,Failures) "
                               ."VALUES "
                               ."('".$G_User->Name()."',0,0,0)");
                    $DB->Query("UPDATE Captcha_UserLog "
                               ."SET Views=Views+1, LastSeen=NOW() "
                               ."WHERE UserName='".$G_User->Name()."'");
                }
            }
        }

        return array("PageName"      => $PageName,
                     "FormName"      => $FormName,
                     "Labels"        => $Labels,
                     "InputElements" => $InputElements,
                     "Notes"         => $Notes);
    }

    /**
    * Verify the captcha code.
    *
    * @return NULL: unable to display captcha
    *    TRUE: Captcha displayed and successfully solved
    *    FALSE: Captcha displayed but solved incorrectly
    */
    public function VerifyCaptcha()
    {
        if ($this->AlreadySolved !== TRUE)
        {
            $valid = NULL;

            # Attempt to validate the captcha, using whatever backend
            # is appropriate for our selected method
            $Method = $this->ConfigSetting("Method");
            if ($Method == "Securimage")
            {
                # For the securimage method:
                if( isset($_POST['captcha_code'])) {
                    include("securimage/securimage.php");
                    $img = new Securimage();
                    $valid = $img->check($_POST['captcha_code']);
                }
            }

            # If we were able to run the validation, we then want
            # to update the IP and User logs to indicate success or failure.
            if ($valid !== NULL)
            {
                $UpdateString = ($valid) ?
                    "SET Successes=Successes+1 ":
                    "SET Failures=Failures+1 ";

                $DB = new Database();
                $DB->Query("UPDATE Captcha_IpLog "
                           .$UpdateString
                           ."WHERE ClientIp='".$_SERVER["REMOTE_ADDR"]."'");

                global $G_User;
                if ($G_User->IsLoggedIn())
                {
                    $DB->Query("UPDATE Captcha_UserLog "
                               .$UpdateString.", LastSeen=NOW() "
                               ."WHERE UserName='".$G_User->Name()."'");
                }
            }

            # And if the validation succeeded, we want to stash that.
            if ($valid === TRUE)
            {
                $_SESSION['CaptchaSolved'] = TRUE;
            }
        }
        else
        {
            $valid = TRUE;
        }

        return $valid;
    }

    /**
    * Captcha verification for resource comments.
    * @param string $Subject Subject to be verified.
    * @param string $Body Body to be verified.
    */
    public function VerifyComment($Subject,$Body)
    {
        $CaptchaStatus = $this->VerifyCaptcha();

        $ErrorMessages = ($CaptchaStatus===FALSE)?
            "Verification Code not correctly entered": "";

        return array($CaptchaStatus, $ErrorMessages);
    }

    /**
    * Captcha verification for user signups.
    * @param string $UserName Name of user being registered.
    * @param string $Password Password that the user provided.
    * @param string $EMail Email address the user provided.
    * @param bool $Status Status of the registration.
    * @return array of updated input values.
    */
    public function VerifySignup($UserName, $Password, $EMail, $Status)
    {
        $CaptchaStatus = $this->VerifyCaptcha();

        if ($CaptchaStatus === FALSE )
        {
            $Status = "U_ERROR_CAPTCHA_FAILURE";
        }

        return array("UserName" => $UserName,
                     "Password" => $Password,
                     "EMail"    => $EMail,
                     "Status"   => $Status);
    }

    /**
    * Add an error message to user signups for captcha failures.
    * @param array $ErrorList Initial list of errors that will be extended.
    */
    public function ExtendSignupErrorList($ErrorList)
    {
        $ErrorList["U_ERROR_CAPTCHA_FAILURE"] =
                "Verification Code not correctly entered";
        return array("ErrorList" => $ErrorList);
    }

    /**
    * If a user has already solved a Captcha during this login session,
    *  do not present them with another one.
    * @param string $PageName Name of the current page.
    */
    public function ManageState($PageName)
    {
        if (isset($_SESSION['CaptchaSolved']) && $_SESSION['CaptchaSolved']===TRUE)
        {
            $this->AlreadySolved = TRUE;
            $_SESSION['CaptchaSolved'] = TRUE;
        }
        else
        {
            $this->AlreadySolved = FALSE;
        }
    }

    /**
    * When a user logs out, clear the flag indicating that they've solved a captcha.
    */
    public function ResetState()
    {
        if (isset($_SESSION['CaptchaSolved']) )
        {
            unset($_SESSION['CaptchaSolved']);
        }
    }

    private $AlreadySolved;

    /**
    * Add the plugin lib directory to the include path for the securimage
    * library.
    */
    private function AddToIncludePath()
    {
        # add the lib path
        set_include_path(get_include_path().PATH_SEPARATOR.dirname(__FILE__)."/lib");
    }
}
