<?PHP

class NavEditor extends Plugin
{

    /**
     * Register information about this plugin.
     */
    public function Register()
    {
        $this->Name = "Navigation Editor";
        $this->Version = "1.0.2";
        $this->Description = "Editor for the primary and secondary lists of"
            ." navigation in CWIS.";
        $this->Author = "Internet Scout";
        $this->Url = "http://scout.wisc.edu/";
        $this->Email = "scout@scout.wisc.edu";
        $this->Requires = array("CWISCore" => "2.1.0");
        $this->EnabledByDefault = TRUE;
    }

    /**
     * Register default configuration settings.
     * @return NULL on success, error message on error
     */
    public function Install()
    {
        $this->ConfigSetting("PrimaryNav", "Home=Home\nBrowse Resources="
            ."BrowseResources\nGet Recommendations=RecommendResources\n"
            ."Forums=Forums\nAbout=About\nHelp=Help");
        $this->ConfigSetting("SecondaryNav", "<b>Register</b>=RegisterPortal=TRUE"
            ."=System Administrator\nPreferences=Preferences=TRUE\nMetadata Tool"
            ."=MDHome=TRUE=System Administrator|Master Resource Administrator|"
            ."Classification Administrator|Controlled Name Administrator|Release"
            ." Flag Administrator\nAdministration=SysAdmin=TRUE=System"
            ." Administrator|Collection Administrator|User Account Administrator\n"
            ."Edit User Account=SelectEditUser=TRUE=System Administrator|User"
            ." Account Administrator\nLog Out=UserLogout=TRUE");
        $this->ConfigSetting("ModifyPrimaryNav", FALSE);
        $this->ConfigSetting("ModifySecondaryNav", FALSE);

        return NULL;
    }

    /**
     * Declare the events this plugin provides to the application framework.
     * @return an array of the events this plugin provides
     */
    public function DeclareEvents()
    {
        return array(
            "NAVEDITOR_GET_CONFIGURATION"
              => ApplicationFramework::EVENTTYPE_FIRST,
            "NAVEDITOR_SET_CONFIGURATION"
              => ApplicationFramework::EVENTTYPE_DEFAULT);
    }

    /**
     * Return event hooks to the application framework.
     * @return an array of events to be hooked into the application framework
     */
    public function HookEvents()
    {
        return array(
            "EVENT_SYSTEM_ADMINISTRATION_MENU" => "DeclareSysAdminPages",
            "EVENT_MODIFY_PRIMARY_NAV" => "ModifyPrimaryNav",
            "EVENT_MODIFY_SECONDARY_NAV" => "ModifySecondaryNav",
            "NAVEDITOR_GET_CONFIGURATION" => "GetConfiguration",
            "NAVEDITOR_SET_CONFIGURATION" => "SetConfiguration");
    }

    /**
     * Add page hooks for the system administration section.
     * @return an array mapping page name to page title
     */
    public function DeclareSysAdminPages()
    {
        return array(
          "EditNavigation" => "Edit Navigation");
    }

    /**
     * Get configuration values.
     * @return an array of configuration values
     */
    public function GetConfiguration()
    {
        $ModifyPrimaryNav = $this->ConfigSetting("ModifyPrimaryNav");
        $PrimaryNav = $this->ConfigSetting("PrimaryNav");
        $ModifySecondaryNav = $this->ConfigSetting("ModifySecondaryNav");
        $SecondaryNav = $this->ConfigSetting("SecondaryNav");

        $Configuration = array(
            "ModifyPrimaryNav" => $ModifyPrimaryNav,
            "PrimaryNav" => $PrimaryNav,
            "ModifySecondaryNav" => $ModifySecondaryNav,
            "SecondaryNav" => $SecondaryNav);

        return $Configuration;
    }

    /**
     * Set a configuration value if it is valid.
     * @param $Key configuration key
     * @param $Value new configuration value
     */
    public function SetConfiguration($Key, $Value)
    {
        if ($Key == "ModifyPrimaryNav" || $Key == "ModifySecondaryNav")
        {
            $SaneValue = (bool) $Value;
            $this->ConfigSetting($Key, $SaneValue);
        }

        else if ($Key == "PrimaryNav" || $Key == "SecondaryNav")
        {
            $this->ConfigSetting($Key, $Value);
        }
    }

    /**
     * Potentially modify the primary nav items.
     * @param $NavItems array of current primary nav items
     * @return the potentially updated primary nav items
     */
    public function ModifyPrimaryNav(array $NavItems)
    {
        $ModifyPrimaryNav = $this->ConfigSetting("ModifyPrimaryNav");
        $PrimaryNav = $this->ConfigSetting("PrimaryNav");
        $OriginalParameters = array("NavItems" => $NavItems);

        if (!$ModifyPrimaryNav)
        {
            return $OriginalParameters;
        }

        $Links = $this->GetLinks($PrimaryNav);

        if (is_null($Links) || count($Links) < 1)
        {
            return $OriginalParameters;
        }

        $NewNavItems = $this->GetNavItems($Links);
        $NewParameters = array("NavItems" => $NewNavItems);

        return $NewParameters;
    }

    /**
     * Potentially modify the secondary nav items.
     * @param $NavItems array of current secondary nav items
     * @return the potentially updated secondary nav items
     */
    public function ModifySecondaryNav(array $NavItems)
    {
        global $User;

        $ModifySecondaryNav = $this->ConfigSetting("ModifySecondaryNav");
        $SecondaryNav = $this->ConfigSetting("SecondaryNav");
        $OriginalParameters = array("NavItems" => $NavItems);

        if (!$ModifySecondaryNav)
        {
            return $OriginalParameters;
        }

        $Links = $this->GetLinks($SecondaryNav);

        if (is_null($Links) || count($Links) < 1)
        {
            return $OriginalParameters;
        }

        $NewNavItems = $this->GetNavItems($Links);

        if ($User->HasPriv(PRIV_SYSADMIN, PRIV_COLLECTIONADMIN, PRIV_USERADMIN))
        {
            $SawSysAdmin = FALSE;

            # look for a link that looks like it leads to the System Administration
            # page to avoid admins accidentally locking themselves out
            foreach ($NewNavItems as $NavItem)
            {
                $ProbablySysAdmin = is_int(strpos($NavItem, "SysAdmin"));

                if ($ProbablySysAdmin)
                {
                    $SawSysAdmin = TRUE;
                    break;
                }
            }

            if (!$SawSysAdmin)
            {
                $NewNavItems["Administration"] = "SysAdmin";
            }
        }

        $NewParameters = array("NavItems" => $NewNavItems);

        return $NewParameters;
    }

    /**
     * Transform a CSV string to an array of NavEditor_Links.
     * @param $Csv CSV string
     * @return an array of NavEditor_Links or NULL if there is an error
     */
    private function GetLinks($Csv)
    {
        $Records = $this->ParseCsv($Csv);

        if (is_null($Records))
        {
            return NULL;
        }

        $Links = $this->TransformCsv($Records);

        return $Links;
    }

    /**
     * Transform an array of NavEditor_Links into an array of nav items, where
     * the key is the label and the value is the page. Does not include links
     * that require the user to be logged in when the user isn't or links that
     * require some privileges the current user doesn't have.
     * @param $Links an array of NavEditor_Links
     * @return an array of pages, with the label as the key
     */
    private function GetNavItems(array $Links)
    {
        global $User;

        $NavItems = array();

        foreach ($Links as $Link)
        {
            $Label = $Link->Label;
            $Page = $Link->Page;
            $DisplayOnlyIfLoggedIn = $Link->DisplayOnlyIfLoggedIn;
            $RequiredPrivileges = $Link->RequiredPrivileges;

            # the user needs to be logged in and isn't
            if ($DisplayOnlyIfLoggedIn && !$User->IsLoggedIn())
            {
                continue;
            }

            # user doesn't have the necessary privileges
            if (!is_null($RequiredPrivileges))
            {
                # an user that isn't logged in won't have the necessary privs
                if (!$User->IsLoggedIn())
                {
                    continue;
                }

                $Callback = array($User, "HasPriv");
                $HasPrivs = call_user_func_array($Callback, $RequiredPrivileges);

                # user doesn't have the required privileges
                if (!$HasPrivs)
                {
                    continue;
                }
            }

            $NavItems[$Label] = $Page;
        }

        return $NavItems;
    }

    /**
     * Parse the given CSV string into an array.
     * @param $Csv CSV string
     * @return an array of CSV records or NULL on error
     */
    private function ParseCsv($Csv)
    {
        # since str_parsecsv() and php://memory aren't always available, a temp
        # file containing the CSV needs to be created so that fgetcsv() can be
        # used instead

        $Handle = @tmpfile();

        if ($Handle === FALSE)
        {
            return NULL;
        }

        $Result = @fwrite($Handle, $Csv);

        if ($Result === FALSE)
        {
            @fclose($Handle);
            return NULL;
        }

        $Result = @fseek($Handle, 0);

        if ($Result === FALSE)
        {
            @fclose($Handle);
            return NULL;
        }

        $Records = array();

        while (FALSE !== ($Record = fgetcsv($Handle, 0, "=")))
        {
            $Records[] = $Record;
        }

        @fclose($Handle);

        return $Records;
    }

    /**
     * Transform an array of CSV records to an array of NavEditor_Link objects,
     * but only those records that are valid.
     * @param $Records CSV records in an array
     * @param an array of NavEditor_Link objects containing valid links
     */
    private function TransformCsv(array $Records)
    {
        $Links = array();

        foreach ($Records as $Record)
        {
            # two fields, label and page, are necesary for all valid links
            if (count($Record) < 2)
            {
                continue;
            }

            # required values
            $Label = $Record[0];
            $Page = $Record[1];

            # optional values
            $DisplayOnlyIfLoggedIn = (count($Record) > 2) ? $Record[2] : NULL;
            $RequiredPrivileges = (count($Record) > 3) ? $Record[3] : NULL;

            # sanitize
            $DisplayOnlyIfLoggedIn = (bool) $DisplayOnlyIfLoggedIn;

            # sanitize
            if (!is_null($RequiredPrivileges))
            {
                $RequiredPrivileges = $this->ParsePrivileges($RequiredPrivileges);

                if (!is_null($RequiredPrivileges))
                {
                    $RequiredPrivileges =
                        $this->TransformPrivileges($RequiredPrivileges);
                }
            }

            $Link = new NavEditor_Link;
            $Link->Label = $Label;
            $Link->Page = $Page;
            $Link->DisplayOnlyIfLoggedIn = $DisplayOnlyIfLoggedIn;
            $Link->RequiredPrivileges = $RequiredPrivileges;

            $Links[] = $Link;
        }

        return $Links;
    }

    /**
     * Parse a string of privilege names in the following format, where
     * everything except the vertical bar is a privilege name: ([^|]*|)*
     * @param $PrivilegeString string of privilege names in the specified format
     * @return an array of the privilege names or NULL on error
     */
    private function ParsePrivileges($PrivilegeString)
    {
        $PrivilegeString = trim($PrivilegeString);

        if (strlen($PrivilegeString) < 1)
        {
            return NULL;
        }

        $Privileges = explode("|", $PrivilegeString);
        $Privileges = array_map("trim", $Privileges);

        return $Privileges;
    }

    /**
     * Transform a list of privilege names in various formats to a list of only
     * those privilege names that are valid along with their values.
     * @param $Privileges an array of privilege names
     * @return an array of valid privileges, with the name as the key
     */
    private function TransformPrivileges(array $Privileges)
    {
        $PrivilegeFactory = new PrivilegeFactory();
        $AllPrivileges = $PrivilegeFactory->GetPrivileges(TRUE, FALSE);
        $PrivilegeConstants = $PrivilegeFactory->GetPredefinedPrivilegeConstants();
        $ValidPrivileges = array();

        foreach ($Privileges as $Privilege)
        {
            # predefined privilege name
            if (in_array($Privilege, $PrivilegeConstants))
            {
                $Key = $Privilege;
                $Value = array_search($Key, $PrivilegeConstants);

                $ValidPrivileges[$Key] = $Value;
            }

            # predefined privilege name without the PRIV_ prefix
            else if (in_array("PRIV_".$Privilege, $PrivilegeConstants))
            {
                $Key = "PRIV_".$Privilege;
                $Value = array_search($Key, $PrivilegeConstants);

                $ValidPrivileges[$Key] = $Value;
            }

            # predefined privilege description or custom privilege name
            else if (in_array($Privilege, $AllPrivileges))
            {
                $Key = $Privilege;
                $Value = array_search($Key, $AllPrivileges);

                $ValidPrivileges[$Key] = $Value;
            }

            else if (array_key_exists($Privilege, $AllPrivileges))
            {
                $Key = $AllPrivileges[$Privilege];
                $Value = $Privilege;

                $ValidPrivileges[$Key] = $Value;
            }
        }

        return $ValidPrivileges;
    }

}
