<?PHP
#
#   FILE:  Mailman.php
#
#   NOTE: View the README file for more information.
#
#   Part of the Collection Workflow Integration System (CWIS)
#   Copyright 2011 Internet Scout Project
#   http://scout.wisc.edu/
#

class Mailman extends Plugin
{

    /**
     * Register information about this plugin.
     */
    public function Register()
    {
        $this->Name = "Mailman";
        $this->Version = "1.0.3";
        $this->Description = "
            Links a CWIS site with a Mailman installation
            to provide user-friendly mailing list subscription
            and transparent updates to mailing list subscriptions when a user's
            e-mail address is changed.";
        $this->Author = "Internet Scout";
        $this->Url = "http://scout.wisc.edu/cwis/";
        $this->Email = "scout@scout.wisc.edu";
        $this->Requires = array("CWISCore" => "2.2.3");
        $this->EnabledByDefault = FALSE;

        $this->CfgSetup["MailmanPrefix"] = array(
            "Type" => "Text",
            "Label" => "Mailman Prefix",
            "Help" => "Installation prefix of the Mailman software");

        $this->CfgSetup["EnableMailingList"] = array(
          "Type" => "Flag",
          "Label" => "Enable Mailing List",
          "Help" => "Enable the default mailing list.",
          "OnLabel" => "Yes",
          "OffLabel" => "No");

        $this->CfgSetup["MailingList"] = array(
          "Type" => "Text",
          "Label" => "Mailing List Name",
          "Help" => "The name of the Mailman mailing list to manage.");

        $this->CfgSetup["MailingListLabel"] = array(
          "Type" => "Text",
          "Label" => "Mailing List Label",
          "Help" => trim(preg_replace('/\s+/', " ", "
              The label to use for the mailing list if the mailing list name
              contains more than two words or is otherwise undesirable to
              display unmodified.")));

        $this->CfgSetup["SubscribeByDefault"] = array(
          "Type" => "Flag",
          "Label" => "Subscribe New Users By Default",
          "Help" => "Check the subscription check box for new users by default.",
          "OnLabel" => "Yes",
          "OffLabel" => "No");

        $this->CfgSetup["UnsubscribeWhenDeleted"] = array(
          "Type" => "Flag",
          "Label" => "Unsubscribe Users When Deleted",
          "Help" => "Unsubscribe users from the mailing list when deleted from CWIS.",
          "OnLabel" => "Yes",
          "OffLabel" => "No");
    }

    /**
     * Install necessary SQL tables.
     * @return NULL on success or error message on error
     */
    public function Install()
    {
        $Database = new Database();

        # table for pending subscriptions
        if (FALSE === $Database->Query("
            CREATE TABLE IF NOT EXISTS Mailman_PendingSubscriptions (
                UserId         INT,
                Created        DATETIME,
                PRIMARY KEY    (UserId)
            );"))
        { return "Could not create the pending subscriptions table."; }

        # set default configuration
        $this->ConfigSetting("MailmanPrefix", NULL);
        $this->ConfigSetting("EnableMailingList", FALSE);
        $this->ConfigSetting("MailingList", NULL);
        $this->ConfigSetting("MailingListLabel", NULL);
        $this->ConfigSetting("SubscribeByDefault", FALSE);
        $this->ConfigSetting("UnsubscribeWhenDeleted", FALSE);

        return NULL;
    }

    /**
     * Upgrade from a previous version.
     * @param $PreviousVersion previous version
     * @return NULL on success or error message on error
     */
    public function Upgrade($PreviousVersion)
    {
        $Database = new Database();

        # remove old lists table, if it's still around
        if (FALSE === $Database->Query("DROP TABLE IF EXISTS MailmanLists"))
        { return "Could not delete the mailing lists table."; }

        # remove old config table, if it's still around
        if (FALSE === $Database->Query("DROP TABLE IF EXISTS MailmanConfig"))
        { return "Could not delete the configuration table."; }

        # ugprade from versions < 1.0.3 to 1.0.3
        if (version_compare($PreviousVersion, "1.0.3", "<"))
        {
            $Database = new Database();

            # table for pending subscriptions
            if (FALSE === $Database->Query("
                CREATE TABLE IF NOT EXISTS Mailman_PendingSubscriptions (
                    UserId         INT,
                    Created        DATETIME,
                    PRIMARY KEY    (UserId)
                );"))
            { return "Could not create the pending subscriptions table."; }
        }

        return NULL;
    }

    /**
     * Make sure the Mailman configuration values are valid
     * @return NULL if there are no errors or an error message otherwise
     */
    public function Initialize()
    {
        $MailmanPrefix = $this->ConfigSetting("MailmanPrefix");

        # if the find_member executable is not found, return an error
        if (!file_exists($MailmanPrefix . "/bin/find_member"))
        {
            return "The \"Mailman Prefix\" setting is invalid.";
        }

        if ($this->DefaultListEnabled())
        {
            $AllLists = $this->GetAllLists();
            $MailingList = $this->ConfigSetting("MailingList");
            $MailingList = $this->NormalizeListName($MailingList);

            # make sure the mailing list is valid
            if (!in_array($MailingList, $AllLists))
            {
                return "The \"Mailman Mailing List\" setting is invalid.";
            }
        }
    }

    /**
     * Declare the events this plugin provides to the application framework.
     * @return an array of the events this plugin provides
     */
    public function DeclareEvents()
    {
        $Events = array(
            "MAILMAN_GET_ALL_LISTS"
                => ApplicationFramework::EVENTTYPE_FIRST,
            "MAILMAN_GET_USER_SUBSCRIPTIONS"
                => ApplicationFramework::EVENTTYPE_FIRST,
            "MAILMAN_IS_SUBSCRIBED"
                => ApplicationFramework::EVENTTYPE_FIRST,
            "MAILMAN_SUBSCRIBE"
                => ApplicationFramework::EVENTTYPE_DEFAULT,
            "MAILMAN_UNSUBSCRIBE"
                => ApplicationFramework::EVENTTYPE_DEFAULT,
            # DEPRECATED EVENTS
            "MAILMAN_EVENT_GET_SUBS"
                => ApplicationFramework::EVENTTYPE_FIRST,
            "MAILMAN_EVENT_CHANGE_SUBS"
                => ApplicationFramework::EVENTTYPE_DEFAULT);

        # these events should only be enabled if the default list is enabled
        if ($this->DefaultListEnabled())
        {
            $Events = array_merge($Events, array(
                "MAILMAN_IS_SUBSCRIBED_TO_DEFAULT_LIST"
                    => ApplicationFramework::EVENTTYPE_FIRST,
                "MAILMAN_SUBSCRIBE_TO_DEFAULT_LIST"
                    => ApplicationFramework::EVENTTYPE_DEFAULT,
                "MAILMAN_UNSUBSCRIBE_FROM_DEFAULT_LIST"
                    => ApplicationFramework::EVENTTYPE_DEFAULT,
                "MAILMAN_GET_DEFAULT_LIST_DISPLAY_NAME"
                    => ApplicationFramework::EVENTTYPE_FIRST));
        }

        return $Events;
    }

    /**
     * Hook the events into the application framework.
     * @return an array of events to be hooked into the application framework
     */
    function HookEvents()
    {
        $Events = array(
            "MAILMAN_GET_ALL_LISTS" => "GetAllLists",
            "MAILMAN_GET_USER_SUBSCRIPTIONS" => "GetUserSubscriptions",
            "MAILMAN_IS_SUBSCRIBED" => "IsSubscribed",
            "MAILMAN_SUBSCRIBE" => "Subscribe",
            "MAILMAN_UNSUBSCRIBE" => "Unsubscribe",
            # DEPRECATED EVENTS
            "MAILMAN_EVENT_GET_SUBS"    => "DEPRECATED_GetUserSubscriptions",
            "MAILMAN_EVENT_CHANGE_SUBS" => "DEPRECATED_ChangeSubscription");

        # these events should only be hooked if the default list is enabled
        if ($this->DefaultListEnabled())
        {
            $Events = array_merge($Events, array(
                "EVENT_MONTHLY" => "RunMonthly",
                "EVENT_PAGE_LOAD" => "PageLoaded",
                "EVENT_USER_ADDED" => "UserAdded",
                "EVENT_USER_VERIFIED" => "UserVerified",
                "EVENT_USER_DELETED" => "UserDeleted",
                "EVENT_USER_EMAIL_CHANGED"  => "UserEmailChanged",
                "EVENT_APPEND_HTML_TO_FORM" => "AppendHtmlToForm",
                "MAILMAN_IS_SUBSCRIBED_TO_DEFAULT_LIST" => "IsSubscribedToDefaultList",
                "MAILMAN_SUBSCRIBE_TO_DEFAULT_LIST" => "SubscribeToDefaultList",
                "MAILMAN_UNSUBSCRIBE_FROM_DEFAULT_LIST" => "UnsubscribeFromDefaultList",
                "MAILMAN_GET_DEFAULT_LIST_DISPLAY_NAME" => "GetDefaultListDisplayName"));
        }

        return $Events;
    }

    /**
     * Delete pending subscriptions that are older than a month.
     * @param $LastRunAt the time this method was last run
     */
    public function RunMonthly($LastRunAt)
    {
        $Database = new Database();

        $Date = date("Y-m-d H:i:s", strtotime("-1 month"));

        $Database->Query("
            DELETE FROM Mailman_PendingSubscriptions
            WHERE Created < ".addslashes($Date));
    }

    /**
     * Manage subscription updates and subscription settings when there are
     * errors in the new account form values.
     * @param $PageName page name
     */
    public function PageLoaded($PageName)
    {
        global $User;

        # catch the form value from the previous page load
        if (isset($_SESSION["P_Mailman_Subscribe"]))
        {
            $_GET["P_Mailman_Subscribe"] = $_SESSION["P_Mailman_Subscribe"];
            unset($_SESSION["P_Mailman_Subscribe"]);
        }

        if ($PageName == "PreferencesComplete")
        {
            $IsSubscribed = $this->IsSubscribedToDefaultList($User);
            $ShouldSubscribe = GetArrayValue($_POST, "P_Mailman_Subscribe");
            $Action = $ShouldSubscribe ?
                "SubscribeToDefaultList" : "UnsubscribeFromDefaultList";

            $this->$Action($User);
        }

        else if ($PageName == "RequestAccountComplete")
        {
            # pass the form value onto the next page load
            $_SESSION["P_Mailman_Subscribe"] =
                GetArrayValue($_POST, "P_Mailman_Subscribe");
        }

        return array("PageName" => $PageName);
    }

    /**
     * Add a newly-added user to the pending subscriptions list if he or she
     * requested a subscription to the default mailing list.
     * @param $UserId user ID
     * @param $Password user password
     */
    public function UserAdded($UserId, $Password)
    {
        $Database = new Database();

        if (GetArrayValue($_POST, "P_Mailman_Subscribe") == "on")
        {
            $Database->Query("
                INSERT INTO Mailman_PendingSubscriptions
                SET UserId = '".addslashes($UserId)."',
                Created = NOW()");
        }
    }

    /**
     * Add a recently-verified user to the default mailing list from the
     * pending subscriptions list if he or she requested a subscription.
     * @param $UserId user ID
     */
    public function UserVerified($UserId)
    {
        $Database = new Database();

        $Database->Query("
            SELECT * FROM Mailman_PendingSubscriptions
            WHERE UserId = '".addslashes($UserId)."'");

        if ($Database->NumRowsSelected() > 0)
        {
            $User = new User($Database, $UserId);
            $this->SubscribeToDefaultList($User);

            $Database->Query("
                DELETE FROM Mailman_PendingSubscriptions
                WHERE UserId = '".addslashes($UserId)."'");
        }
    }

    /**
     * Remove a recently-deleted user from the default mailing list if the
     * plugin is configured to do so.
     * @param $UserId user ID
     */
    public function UserDeleted($UserId)
    {
        $Database = new Database();

        if ($this->ConfigSetting("UnsubscribeWhenDeleted"))
        {
            $User = new User($Database, $UserId);
            $this->UnsubscribeFromDefaultList($User);

            $Database->Query("
                DELETE FROM Mailman_PendingSubscriptions
                WHERE UserId = '".addslashes($UserId)."'");
        }
    }

    /**
     * Update the user's mailing list subscriptions when his or her e-mail
     * address has changed.
     * @param $UserId user ID
     * @param $OldEmail previous e-mail address for the user
     * @param $NewEmail new e-mail address for the user
     */
    public function UserEmailChanged($UserId, $OldEmail, $NewEmail)
    {
        $User = new User(new Database(), $UserId);
        $ListSubscriptions = $this->MailmanGetUserSubscriptions($OldEmail);

        foreach ($ListSubscriptions as $List)
        {
            # unsubscribe the user from the list with his or her old e-mail
            $this->MailmanUnsubscribe($OldEmail, $List);

            # subscribe the user to the list with his or her new e-mail
            $this->MailmanSubscribe($NewEmail, $List);
        }
    }

    /**
     * Add a "subscribe" option to the new account form to allow users to
     * subscribe to the default Mailman mailing list.
     * @param $Page page name
     * @param $Form form name
     * @param $Labels array of form labels
     * @param $Inputs array of form input elements
     * @param $Notes array of notes for the labels and inputs
     */
    public function AppendHtmlToForm($Page, $Form, $Labels, $Inputs, $Notes)
    {
        global $User;

        # allow new users to subscribe to the mailing list
        if ($Page == "RequestAccount" && $Form == "NewAccountForm")
        {
            $DisplayName = $this->GetDefaultListDisplayName();
            $ShouldSubscribe = GetArrayValue($_GET, "P_Mailman_Subscribe");
            $ShouldSubscribe = $ShouldSubscribe ||
                (!isset($_GET["FTAddErrCodes"]) &&
                $this->ConfigSetting("SubscribeByDefault"));
            $Checked = $ShouldSubscribe ? ' checked="checked"' : NULL;

            $Labels[] = NULL;
            $Notes[] = NULL;
            $Inputs[] = '
                <input type="checkbox" id="P_Mailman_Subscribe" name="P_Mailman_Subscribe"'.$Checked.' />
                <label for="P_Mailman_Subscribe">Subscribe to <i>' . $DisplayName . '</i></label>';
        }

        # allow existing users to modify their subscription
        else if ($Page == "Preferences" && $Form == "UserPreferences")
        {
            $DisplayName = defaulthtmlentities($this->GetDefaultListDisplayName());
            $IsSubscribed = $this->IsSubscribedToDefaultList($User);
            $YesChecked = $IsSubscribed ? ' checked="checked"' : NULL;
            $NoChecked = $IsSubscribed ? NULL : ' checked="checked"';

            $Notes[] = NULL;
            $Labels[] = '<label for="P_Mailman_Subscribe">Subscribe to <i>' . $DisplayName . '</i></label>';
            $Inputs[] = '
                <input type="radio" id="P_Mailman_Subscribe_Yes" name="P_Mailman_Subscribe" value="1"'.$YesChecked.' />
                <label for="P_Mailman_Subscribe_Yes">Yes</label>

                <input type="radio" id="P_Mailman_Subscribe_No" name="P_Mailman_Subscribe" value="0"'.$NoChecked.' />
                <label for="P_Mailman_Subscribe_No">No</label>';
        }

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

    /**
     * Get all available mailman mailing lists.
     * @return an array of all available mailing lists
     */
    public function GetAllLists()
    {
        $Binary = $this->ConfigSetting("MailmanPrefix") . "/bin/list_lists";
        $SafeBinary = escapeshellarg($Binary);

        $Mailman = "sudo -u mailman " . $SafeBinary;
        $Options = "--bare";
        $Command = $Mailman . " " . $Options;

        exec($Command, $Lists);

        return $Lists;
    }

    /**
     * Get the Mailman mailing lists to which the given user is subscribed.
     * @param $User User object
     * @return an array of mailing list to which the user is subscribed
     */
    public function GetUserSubscriptions(User $User)
    {
        $Email = $User->Get("EMail");

        return $this->MailmanGetUserSubscriptions($Email);
    }

    /**
     * Determine if the given user is subscribed to the given Mailman mailing
     * list.
     * @param $User User object
     * @param $List mailing list name
     * @return TRUE if the user is subscribed to the mailing list
     */
    public function IsSubscribed(User $User, $List)
    {
        $ListSubscriptions = $this->GetUserSubscriptions($User);
        $List = $this->NormalizeListName($List);

        $IsSubscribed = in_array($List, $ListSubscriptions);

        return $IsSubscribed;
    }

    /**
     * Subscribe the given user to the given Mailman mailing list.
     * @param $User User object
     * @param $List mailing list
     */
    public function Subscribe(User $User, $List)
    {
        $AllLists = $this->GetAllLists();
        $List = $this->NormalizeListName($List);

        # don't try subscribing the user to a non-existent list
        if (!in_array($List, $AllLists))
        {
            return;
        }

        $this->MailmanSubscribe($User->Get("EMail"), $List);
    }

    /**
     * Unsubscribe the given user from the given Mailman mailing list.
     * @param $User User object
     * @param $List mailing list
     */
    public function Unsubscribe(User $User, $List)
    {
        $AllLists = $this->GetAllLists();
        $List = $this->NormalizeListName($List);

        # don't try unsubscribing the user to a non-existent list
        if (!in_array($List, $AllLists))
        {
            return;
        }

        $this->MailmanUnsubscribe($User->Get("EMail"), $List);
    }

    /**
     * Determine if the given user is subscribed to the default Mailman mailing
     * list.
     * @param $User User object
     * @return TRUE if the user is subscribed to the default mailing list
     */
    public function IsSubscribedToDefaultList(User $User)
    {
        $ListSubscriptions = $this->GetUserSubscriptions($User);
        $DefaultList = $this->ConfigSetting("MailingList");
        $DefaultList = $this->NormalizeListName($DefaultList);

        $IsSubscribed = in_array($DefaultList, $ListSubscriptions);

        return $IsSubscribed;
    }

    /**
     * Subscribe the given user to the default Mailman mailing list.
     * @param $User User object
     */
    public function SubscribeToDefaultList(User $User)
    {
        $DefaultList = $this->ConfigSetting("MailingList");
        $DefaultList = $this->NormalizeListName($DefaultList);

        $this->Subscribe($User, $DefaultList);
    }

    /**
     * Unsubscribe the given user from the default Mailman mailing list.
     * @param $User User object
     */
    public function UnsubscribeFromDefaultList(User $User)
    {
        $DefaultList = $this->ConfigSetting("MailingList");
        $DefaultList = $this->NormalizeListName($DefaultList);

        $this->Unsubscribe($User, $DefaultList);
    }

    /**
     * Get the display name for the default Mailman mailing list. It will return
     * the mailing list label or the mailing list name if the label is not set.
     * @return the display name for the default Mailman mailing list
     */
    public function GetDefaultListDisplayName()
    {
        $DisplayName = $this->ConfigSetting("MailingListLabel");

        # use the mailing list name if the label is unavailable
        if (is_null($DisplayName) || strlen(trim($DisplayName)) < 1)
        {
            $DisplayName = $this->ConfigSetting("MailingList");
        }

        return $DisplayName;
    }

    /**
     * Get the mailing list subscriptions for the given e-mail address via a
     * shell.
     * @param $Email e-mail address
     * @return an array of mailing list to which the user is subscribed
     */
    protected function MailmanGetUserSubscriptions($Email)
    {
        # escape shell arguments
        $Binary = $this->ConfigSetting("MailmanPrefix") . "/bin/find_member";
        $SafeBinary = escapeshellarg($Binary);
        $SafeEmail = escapeshellarg("^" . $Email . "$");

        # construct the shell command
        $Mailman = "sudo -u mailman " . $SafeBinary;
        $Options = "--owners";
        $Command = $Mailman . " " . $Options . " " . $SafeEmail;

        exec($Command, $Lines);

        $Lists = array();

        foreach ($Lines as $Line)
        {
            # skip over ". . . found in:" lines
            if (preg_match('/^     (.+?)$/', $Line, $Matches))
            {
                $Lists[] = $Matches[1];
            }
        }

        # remove redundant list names
        $Lists = array_unique($Lists);

        return $Lists;
    }

    /**
     * Subscribe the given e-mail address to the given list via a shell.
     * @param $Email e-mail address
     * @param $List mailing list name
     */
    protected function MailmanSubscribe($Email, $List)
    {
        # escape shell arguments
        $Binary = $this->ConfigSetting("MailmanPrefix") . "/bin/add_members";
        $SafeBinary = escapeshellarg($Binary);
        $SafePrefix = escapeshellarg($this->ConfigSetting("MailmanPrefix"));
        $SafeEmail = escapeshellarg($Email);
        $SafeList = escapeshellarg($List);

        # construct the shell command
        $Echo = "echo " . $SafeEmail;
        $Mailman = "sudo -u mailman " . $SafeBinary;
        $Options = "--regular-members-file=- --welcome-msg=n --admin-notify=n";
        $Command = $Echo . " | " . $Mailman . " " . $Options . " " . $SafeList;

        exec($Command);
    }

    /**
     * Unsubscribe the given e-mail address from the given list via a shell.
     * @param $Email e-mail address
     * @param $List mailing list name
     */
    protected function MailmanUnsubscribe($Email, $List)
    {
        # escape shell arguments
        $Binary = $this->ConfigSetting("MailmanPrefix") . "/bin/remove_members";
        $SafeBinary = escapeshellarg($Binary);
        $SafePrefix = escapeshellarg($this->ConfigSetting("MailmanPrefix"));
        $SafeEmail = escapeshellarg($Email);
        $SafeList = escapeshellarg($List);

        # construct the shell command
        $Echo = "echo " . $SafeEmail;
        $Mailman = "sudo -u mailman " . $SafeBinary;
        $Options = "--file=- --nouserack --noadminack";
        $Command = $Echo . " | " . $Mailman . " " . $Options . " " . $SafeList;

        exec($Command, $Output, $ReturnValue);
    }

    /**
     * Normalize the given mailing list name.
     * @param $ListName mailing list name
     * @return normalize mailing list name
     */
    protected function NormalizeListName($ListName)
    {
        return strtolower(trim($ListName));
    }

    /**
     * Determine whether the default mailing list is enabled in CWIS.
     * @return TRUE if the default mailing list is enabled or FALSE otherwise
     */
    protected function DefaultListEnabled()
    {
        return $this->ConfigSetting("EnableMailingList");
    }

    # ----- DEPRECATED --------------------------------------------------------

    /**
     * DEPRECATED: Get the mailing list subscriptions of the current user.
     * @return an array of mailing list to which the user is subscribed
     */
    public function DEPRECATED_GetUserSubscriptions()
    {
        global $User;

        return $this->GetUserSubscriptions($User);
    }

    /**
     * DEPRECATED: Changed a mailing list subscription of the current user.
     * @param $List mailing list name
     * @param $Subscribe TRUE to subscribe the user and FALSE to unsubscribe
     */
    public function DEPRECATED_ChangeSubscription($List, $Subscribe)
    {
        global $User;

        $Action = $Subscribe ? "Subscribe" : "Unsubscribe";

        $this->$Action($User, $List);
    }

}
