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

/**
* Plugin to provide support for serving resource metadata up via OAI-PMH.
*/
class OAIPMHServer extends Plugin {

    # ---- STANDARD PLUGIN INTERFACE -----------------------------------------

    /**
    * Set the plugin attributes.  At minimum this method MUST set $this->Name
    * and $this->Version.  This is called when the plugin is initially loaded.
    */
    function Register()
    {
        $this->Name = "OAI-PMH Server";
        $this->Version = "1.0.5";
        $this->Description = "Provides support for"
                ." serving up resource records using version 2.0 of the <a"
                ." href=\"http://www.openarchives.org/OAI/openarchivesprotocol.html\""
                ." target=\"_blank\">Open Archives"
                ." Initiative Protocol for Metadata Harvesting</a> (OAI-PMH).";
        $this->Author = "Internet Scout";
        $this->Url = "http://scout.wisc.edu/cwis/";
        $this->Email = "scout@scout.wisc.edu";
        $this->Requires = array("CWISCore" => "2.9.0");
        $this->EnabledByDefault = TRUE;

        $this->CfgSetup["AutomaticRequestDetection"] = array(
                "Label" => "Automatic OAI-PMH Request Detection",
                "Type" => "Flag",
                "Default" => TRUE,
                "Help" => "When enabled, the plugin will attempt to"
                        ." automatically detect harvest requests, regardless"
                        ." of the invoking URL."
                        ." This may need to be disabled when working with a"
                        ." harvester that imposes strict validation checks."
                );
    }

    /**
    * Perform any work needed when the plugin is first installed (for example,
    * creating database tables).
    * @return NULL if installation succeeded, otherwise a string containing
    *       an error message indicating why installation failed.
    */
    function Install()
    {
        # set up configuration defaults
        global $G_SysConfig;
        $RepDescr["Name"] = $G_SysConfig->PortalName();
        $RepDescr["AdminEmail"] = array($G_SysConfig->AdminEmail());
        $Protocol = isset($_SERVER["HTTPS"]) ? "https://" : "http://";
        $ServerName = ($_SERVER["SERVER_NAME"] != "127.0.0.1")
                        ? $_SERVER["SERVER_NAME"]
                        : $_SERVER["HTTP_HOST"];
        $ServerName = str_replace('/','',$ServerName);
        $ServerName = ($ServerName == "localhost")
                ? gethostname() : $ServerName;
        $RepDescr["BaseURL"] = $Protocol.$ServerName.$_SERVER["SCRIPT_NAME"];
        $RepDescr["IDDomain"] = $ServerName;
        $RepDescr["IDPrefix"] = $ServerName;
        $RepDescr["DateGranularity"] = "DATE";
        $RepDescr["EarliestDate"] = "1990-01-01";
        $this->ConfigSetting("RepositoryDescr", $RepDescr);
        $this->ConfigSetting("SQEnabled", TRUE);

        # copy over old configuration info (if any)
        $this->TransferLegacyConfiguration();

        # look for format outline files in install directory
        $this->FormatFileLocation = dirname(__FILE__)."/install";

        # initialize/expand formats from format outlines
        $this->LoadFormatsFromOutlines();

        # build and add native format
        $NativeFormatSuffix = substr(strtolower(preg_replace(
                "/[^a-zA-Z0-9]/", "", $RepDescr["Name"])), 0, 8);
        $this->AddNativeFormat($NativeFormatSuffix, $RepDescr["BaseURL"]);

        # report installation error if no oai_dc format found
        $Formats = $this->ConfigSetting("Formats");
        if (!isset($Formats["oai_dc"]))
        {
            return "Required oai_dc format not found.";
        }
    }

    /**
    * Initialize the plugin.  This is called after all plugins have been
    * loaded but before any methods for this plugin (other than Register()
    * or Initialize()) have been called.
    * @return NULL if initialization was successful, otherwise a string
    *       containing an error message indicating why initialization failed.
    */
    function Initialize()
    {
        $this->AddToIncludePath();

        # add clean URL for harvest request
        $GLOBALS["AF"]->AddCleanUrl("%^OAI$%", "P_OAIPMHServer_OAI");

        # report success to caller
        return NULL;
    }

    /**
    * Hook the events into the application framework.
    * @return Returns an array of events to be hooked into the application
    *      framework.
    */
    function HookEvents()
    {
        $Hooks = array(
                "EVENT_COLLECTION_ADMINISTRATION_MENU" => "AddCollectionAdminMenuItems",
                "EVENT_SYSTEM_INFO_LIST" => "AddSystemInfoListItems",
                );
        if ($this->ConfigSetting("AutomaticRequestDetection"))
        {
            $Hooks["EVENT_PHP_FILE_LOAD"] = "CheckForOaiRequest";
        }
        return $Hooks;
    }

    /**
    * Declare events defined by this plugin.  This is used when a plugin defines
    * new events that it signals or responds to.  Names of these events should
    * begin with the plugin base name, followed by "_EVENT_" and the event name
    * in all caps (for example "MyPlugin_EVENT_MY_EVENT").
    * @return Array with event names for the index and event types for the values.
    */
    function DeclareEvents()
    {
        return array(
                "OAIPMHServer_EVENT_MODIFY_RESOURCE_SEARCH_PARAMETERS"
                        => ApplicationFramework::EVENTTYPE_CHAIN,
                "OAIPMHServer_EVENT_FILTER_RESULTS"
                        => ApplicationFramework::EVENTTYPE_CHAIN,
                );
    }


    # ---- HOOKED METHODS ----------------------------------------------------

    function AddCollectionAdminMenuItems()
    {
        return array("EditConfig" => "OAI Server Configuration");
    }

    function AddSystemInfoListItems()
    {
        $RepDescr = $this->ConfigSetting("RepositoryDescr");
        $ServerUrl = $RepDescr["BaseURL"];
        $OaiExplorerUrl = "http://re.cs.uct.ac.za/cgi-bin/Explorer/2.0-1.46/"
                ."testoai?metadataPrefix=oai_dc&archive=".urlencode($ServerUrl);
        return array("OAI-PMH Server Base URL" =>
                "<a href=\"".htmlspecialchars($ServerUrl)."\" target=\"_blank\">"
                        .htmlspecialchars($ServerUrl)."</a>"
                        ."&nbsp;&nbsp;&nbsp;&nbsp;"
                        ."<a class=\"cw-button cw-button-constrained cw-button-elegant\""
                        ." href=\""
                        .htmlspecialchars($OaiExplorerUrl)
                        ."\" target=\"_blank\">TEST</a>"
                );
    }

    /**
    * Check GET parameters for OAI-PMH request and redirect to our OAI server
    * page if found.  (HOOKED to EVENT_PHP_FILE_LOAD)
    * @param PageName PHP file name.
    * @return PHP file name, possibly changed to load our page.
    */
    function CheckForOaiRequest($PageName)
    {
        # if valid harvesting command appears to be present
        $ValidVerbs = array(
                "GetRecord",
                "Identify",
                "ListIdentifiers",
                "ListMetadataFormats",
                "ListRecords",
                "ListSets",
                );
        $Verb = GetFormValue("verb");
        if (in_array($Verb, $ValidVerbs))
        {
            # redirect to our page if required arguments for command are present
            switch ($Verb)
            {
                case "GetRecord":
                    if (GetFormValue("identifier")
                            && GetFormValue("metadataPrefix"))
                    {
                        $PageName = "P_OAIPMHServer_OAI.php";
                    }
                    break;

                case "ListIdentifiers":
                case "ListRecords":
                    if (GetFormValue("metadataPrefix")
                            || GetFormValue("resumptionToken"))
                    {
                        $PageName = "P_OAIPMHServer_OAI.php";
                    }
                    break;

                default:
                    $PageName = "P_OAIPMHServer_OAI.php";
                    break;
            }
        }
        return array("PageName" => $PageName);
    }


    # ----- PRIVATE METHODS ------------------------------------------------

    private $FormatFileLocation;

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

    /**
    * Load XML format outline files.
    * @return Array containing format information.
    */
    private function LoadFormatOutlines()
    {
        # for all files in format file location
        $Formats = array();
        $FileList = scandir($this->FormatFileLocation);
        foreach ($FileList as $FileName)
        {
            # if file looks like a format file
            if (preg_match("/^Format--[a-zA-Z0-9_-]+\\.xml\$/i", $FileName))
            {
                # read in format from file
                $FullFileName = realpath($this->FormatFileLocation."/".$FileName);
                $Xml = simplexml_load_file($FullFileName);
                if (isset($Xml->formatName))
                {
                    $Formats[(string)$Xml->formatName] = SimpleXMLToArray($Xml);
                }
            }
        }

        # return loaded formats to caller
        return $Formats;
    }

    /**
    * Transfer over legacy OAI-PMH server configuration.
    */
    private function TransferLegacyConfiguration()
    {
        # if old OAI-PMH configuration is available
        $DB = new Database();
        if ($DB->FieldExists("SystemConfiguration", "OaiIdDomain"))
        {
            # copy base configuration from legacy OAI-PMH server support values
            $RepDescr = $this->ConfigSetting("RepositoryDescr");
            global $G_SysConfig;
            if (strlen(trim($G_SysConfig->OaiIdDomain())))
                    $RepDescr["IDDomain"] = $G_SysConfig->OaiIdDomain();
            if (strlen(trim($G_SysConfig->OaiIdPrefix())))
                    $RepDescr["IDPrefix"] = $G_SysConfig->OaiIdPrefix();
            if (($G_SysConfig->OaiDateGranularity() == "DATE")
                    || ($G_SysConfig->OaiDateGranularity() == "DATETIME"))
                    $RepDescr["DateGranularity"] = $G_SysConfig->OaiDateGranularity();
            if (strlen(trim($G_SysConfig->OaiEarliestDate())))
                    $RepDescr["EarliestDate"] = $G_SysConfig->OaiEarliestDate();
            $this->ConfigSetting("RepositoryDescr", $RepDescr);
            $this->ConfigSetting("SQEnabled", $G_SysConfig->OAISQEnabled());

            # copy existing field mappings
            $DB->Query("SELECT * FROM OAIFieldMappings");
            while ($Record = $DB->FetchRow())
            {
                if ($Record["OAIFieldName"] != "Unmapped")
                {
                    $Formats[$Record["FormatName"]]
                            ["Elements"][$Record["OAIFieldName"]]
                            = $Record["SPTFieldId"];
                }
            }

            # copy existing qualifier mappings
            $DB->Query("SELECT * FROM OAIQualifierMappings");
            while ($Record = $DB->FetchRow())
            {
                if ($Record["OAIQualifierName"] != "Unmapped")
                {
                    $Formats[$Record["FormatName"]]
                            ["Qualifiers"][$Record["OAIQualifierName"]]
                            = $Record["SPTQualifierId"];
                }
            }
            $this->ConfigSetting("Formats", $Formats);
        }
    }

    /**
    * Load new formats from format outline files.
    */
    private function LoadFormatsFromOutlines()
    {
        # load current formats
        $Formats = $this->ConfigSetting("Formats");

        # load format outlines from files
        $FormatOutlines = $this->LoadFormatOutlines();

        # for each loaded format outline
        foreach ($FormatOutlines as $FormatName => $Outline)
        {
            # save any needed basic format info
            if (!isset($Formats[$FormatName]["TagName"]))
                    $Formats[$FormatName]["TagName"] = $Outline["tagName"];
            if (!isset($Formats[$FormatName]["SchemaNamespace"]))
                    $Formats[$FormatName]["SchemaNamespace"]
                            = $Outline["schema"]["namespace"];
            if (!isset($Formats[$FormatName]["SchemaDefinition"]))
                    $Formats[$FormatName]["SchemaDefinition"]
                            = $Outline["schema"]["definition"];
            if (!isset($Formats[$FormatName]["SchemaVersion"])
                    && is_string($Outline["schema"]["version"]))
                    $Formats[$FormatName]["SchemaVersion"]
                            = $Outline["schema"]["version"];

            # if there are no namespaces set for this format
            if (!isset($Formats[$FormatName]["Namespaces"])
                    || !count($Formats[$FormatName]["Namespaces"]))
            {
                # if there are namespaces for this format outline
                $Formats[$FormatName]["Namespaces"] = array();
                if (isset($Outline["namespace"]))
                {
                    # convert namespace to array if necessary
                    if (!isset($Outline["namespace"][0]))
                    {
                        $Outline["namespace"] = array($Outline["namespace"]);
                    }

                    # for each namespace
                    foreach ($Outline["namespace"] as $Namespace)
                    {
                        # if mapping looks viable
                        if (isset($Namespace["name"]) && strlen($Namespace["name"])
                                && isset($Namespace["uri"])
                                && strlen($Namespace["uri"]))
                        {
                            # map namespace
                            $Formats[$FormatName]["Namespaces"][$Namespace["name"]]
                                    = $Namespace["uri"];
                        }
                    }
                }
            }

            # if there are no elements set for this format
            if (!isset($Formats[$FormatName]["Elements"])
                    || !count($Formats[$FormatName]["Elements"]))
            {
                # if there are element mappings for this format outline
                $Formats[$FormatName]["Elements"] = array();
                if (isset($Outline["element"]))
                {
                    # convert element mapping to array if necessary
                    if (!isset($Outline["element"][0]))
                    {
                        $Outline["element"] = array($Outline["element"]);
                    }

                    # for each element mapping
                    $Schema = new MetadataSchema();
                    foreach ($Outline["element"] as $Element)
                    {
                        # if mapping looks viable
                        if (isset($Element["name"]) && strlen($Element["name"]))
                        {
                            # map element
                            $Formats[$FormatName]["Elements"][$Element["name"]]
                                    = $this->MapOutlineName($Element, $Schema, TRUE);
                        }
                    }
                }
            }

            # if there are no qualifiers set for this format
            if (!isset($Formats[$FormatName]["Qualifiers"])
                    || !count($Formats[$FormatName]["Qualifiers"]))
            {
                # if there are qualifier mappings for this format outline
                $Formats[$FormatName]["Qualifiers"] = array();
                if (isset($Outline["qualifier"]))
                {
                    # convert qualifier mapping to array if necessary
                    if (!isset($Outline["qualifier"][0]))
                    {
                        $Outline["qualifier"] = array($Outline["qualifier"]);
                    }

                    # for each qualifier mapping
                    $QFactory = new QualifierFactory();
                    foreach ($Outline["qualifier"] as $Qualifier)
                    {
                        # if mapping looks viable
                        if (isset($Qualifier["name"]) && strlen($Qualifier["name"]))
                        {
                            # map qualifier
                            $Formats[$FormatName]["Qualifiers"][$Qualifier["name"]]
                                    = $this->MapOutlineName($Element, $QFactory);
                        }
                    }
                }
            }
        }

        # save updated formats
        $this->ConfigSetting("Formats", $Formats);
    }

    /**
    * Add format built off of native schema.
    */
    private function AddNativeFormat($FormatNameSuffix, $BaseUrl)
    {
        # set up format description
        $FormatNameSuffix = trim($FormatNameSuffix);
        if (!strlen($FormatNameSuffix)) {  $FormatNameSuffix = "xxx";  }
        $Format["FormatName"] = "native_".$FormatNameSuffix;
        $Format["TagName"] = $FormatNameSuffix;
        $Format["SchemaNamespace"] = $BaseUrl."/".$FormatNameSuffix;
        $Format["SchemaDefinition"] = $BaseUrl."/"."XSD/file/path/goes/here.xsd";
        $Format["SchemaVersion"] = "1.0.0";

        # swipe namespaces and qualifiers from nsdl_dc format
        $Formats = $this->ConfigSetting("Formats");
        $Format["Namespaces"] = $Formats["nsdl_dc"]["Namespaces"];
        $Format["Qualifiers"] = $Formats["nsdl_dc"]["Qualifiers"];

        # for each currently enabled metadata field
        $Format["Elements"] = array();
        $Schema = new MetadataSchema();
        $Fields = $Schema->GetFields();
        foreach ($Fields as $FieldId => $Field)
        {
            # normalize metadata field name to create OAI element name
            $ElementName = preg_replace("/[^a-zA-Z0-9]/", "", $Field->Name());
            $ElementName{0} = strtolower($ElementName{0});
            $ElementName = $ElementName;

            # add element mapping to format
            $Format["Elements"][$ElementName] = $FieldId;
        }

        # save new format
        $Formats[$Format["FormatName"]] = $Format;
        $this->ConfigSetting("Formats", $Formats);
    }

    /**
    * Map recommended field name (from format outline file) to metadata field ID.
    * @param Element Associative array with element info.
    * @param Factory ItemFactory-derived factory object to query for mappable items.
    * @param LookForStdName Call factory method to search for standard mappings.
    * @return Metadata field ID or -1 if no appropriate field found.
    */
    private function MapOutlineName($Element, $Factory, $LookForStdName = FALSE)
    {
        # return -1 if no mapping found
        $FieldId = -1;

        # if recommended mapping was supplied
        if (isset($Element["recommendedMapping"]))
        {
            # build list of recommended mappings
            $Recommendations = array($Element["recommendedMapping"]);
            if (isset($Element["alternateMapping"]))
            {
                if (is_array($Element["alternateMapping"]))
                {
                    $Recommendations = array_merge($Recommendations,
                            $Element["alternateMapping"]);
                }
                else
                {
                    $Recommendations[] = $Element["alternateMapping"];
                }
            }

            # for each recommended mapping
            foreach ($Recommendations as $Name)
            {
                # look for field with supplied name
                $FieldId = $Factory->GetItemByName($Name, TRUE);

                # if field found
                if ($FieldId !== FALSE)
                {
                    # stop searching
                    break;
                }
                elseif ($LookForStdName)
                {
                    # look for field mapped to supplied standard name
                    $FieldId = $Schema->StdNameToFieldMapping($Name);

                    # if field mapped to supplied standard name found
                    if ($FieldId !== NULL)
                    {
                        # stop searching
                        break;
                    }
                }

                # no mapping found so restore -1 field ID to indicate none found
                $FieldId = -1;
            }
        }

        # return ID of mapped field to caller
        return $FieldId;
    }
}
