<commit_before><?php

class EngineBlock_Corto_ProxyServer
{
    const ID_PREFIX = 'CORTO';

    const MODULE_BINDINGS   = 'Bindings';
    const MODULE_SERVICES   = 'Services';

    const TEMPLATE_SOURCE_FILESYSTEM = 'filesystem';
    const TEMPLATE_SOURCE_MEMORY     = 'memory';

    const MESSAGE_TYPE_REQUEST  = 'SAMLRequest';
    const MESSAGE_TYPE_RESPONSE = 'SAMLResponse';

    const VO_CONTEXT_PFX          = 'voContext';

    protected $_serviceToControllerMapping = array(
        'singleSignOnService'               => '/authentication/idp/single-sign-on',
        'debugSingleSignOnService'          => '/authentication/sp/debug',
        'continueToIdP'                     => '/authentication/idp/process-wayf',

        'assertionConsumerService'          => '/authentication/sp/consume-assertion',
        'continueToSP'                      => '/authentication/sp/process-consent',
        'provideConsentService'             => '/authentication/idp/provide-consent',
        'processConsentService'             => '/authentication/idp/process-consent',
        'processedAssertionConsumerService' => '/authentication/proxy/processed-assertion',

        'idpMetadataService'                => '/authentication/idp/metadata',
        'spMetadataService'                 => '/authentication/sp/metadata',
    );

    protected $_headers = array();
    protected $_output;

    protected $_voContext = null;

    protected $_requestArray;
    protected $_responseArray;

    protected $_server;
    protected $_systemLog;
    protected $_sessionLog;
    protected $_sessionLogDefault;

    protected $_configs;
    protected $_entities = array(
        'current'=>array(),
        'hosted'=>array(),
        'remote'=>array(),
    );
    protected $_modules = array();
    protected $_templateSource;
    protected $_processingMode = false;

    public function __construct()
    {
        $this->_server = $this;
    }

//////// GETTERS / SETTERS /////////


    public function setVirtualOrganisationContext($voContext)
    {
        $this->_voContext = $voContext;
    }

    public function getVirtualOrganisationContext()
    {
        return $this->_voContext;
    }

    public function getOutput()
    {
        return $this->_output;
    }

    public function getHeaders()
    {
        return $this->_headers;
    }

    public function sendOutput($rawOutput)
    {
        $this->_output = $rawOutput;
    }

    public function sendHeader($name, $value)
    {
        $this->_headers[$name] = $value;
    }

    public function setProcessingMode()
    {
        $this->_processingMode = true;
        return $this;
    }

    public function unsetProcessingMode()
    {
        $this->_processingMode = false;
        return $this;
    }

    public function isInProcessingMode()
    {
        return $this->_processingMode;
    }

    /**
     * Does the request have an "unsolicited" flag
     *
     * @param array $request
     * @return type
     */
    public function isUnsolicitedRequest(array $request)
    {
        return !empty($request[EngineBlock_Corto_XmlToArray::PRIVATE_PFX]['Unsolicited']);
    }

    /**
     * @return EngineBlock_Corto_Module_Bindings
     */
    public function getBindingsModule()
    {
        return $this->_getModule(self::MODULE_BINDINGS);
    }

    /**
     * @param EngineBlock_Corto_Module_Bindings $bindingsInstance
     * @return EngineBlock_Corto_ProxyServer
     */
    public function setBindingsModule(EngineBlock_Corto_Module_Bindings $bindingsInstance)
    {
        return $this->_setModule(self::MODULE_BINDINGS, $bindingsInstance);
    }

    /**
     * @return EngineBlock_Corto_Module_Services
     */
    public function getServicesModule()
    {
        return $this->_getModule(self::MODULE_SERVICES);
    }

    /**
     * @param EngineBlock_Corto_Module_Services $servicesInstance
     * @return EngineBlock_Corto_ProxyServer
     */
    public function setServicesModule(EngineBlock_Corto_Module_Services $servicesInstance)
    {
        return $this->_setModule(self::MODULE_SERVICES, $servicesInstance);
    }

    /**
     * @param string $name
     * @return EngineBlock_Corto_Module_Abstract
     */
    protected function _getModule($name)
    {
        return $this->_modules[$name];
    }

    /**
     * @param  $name
     * @param  $moduleInstance
     * @return EngineBlock_Corto_ProxyServer
     */
    protected function _setModule($name, EngineBlock_Corto_Module_Abstract $moduleInstance)
    {
        $this->_modules[$name] = $moduleInstance;
        return $this;
    }

    public function getConfig($name, $default = null)
    {
        if (isset($this->_configs[$name])) {
            return $this->_configs[$name];
        }
        return $default;
    }

    public function getConfigs()
    {
        return $this->_configs;
    }

    public function setConfig($name, $value)
    {
        $this->_configs[$name] = $value;
        return $this;
    }

    public function setConfigs($configs)
    {
        $this->_configs = $configs;
        return $this;
    }

    public function getAttributeName($attributeId, $ietfLanguageTag = 'en', $fallbackToId = true)
    {
        $metadata = new EngineBlock_Attributes_Metadata();
        return $metadata->getName($attributeId, $ietfLanguageTag, $fallbackToId);
    }

    public function getAttributeDescription($attributeId, $ietfLanguageTag = 'en', $fallbackToId = true)
    {
        $metadata = new EngineBlock_Attributes_Metadata();
        return $metadata->getDescription($attributeId, $ietfLanguageTag);
    }

    /**
     * Return the url of the Static vhost containing media, script and css files
     *
     * @example <?php echo $this->staticUrl(); ?>
     *
     * @return string
     */
    public static function staticUrl($path = "")
    {
        $application = EngineBlock_ApplicationSingleton::getInstance();
        $settings = $application->getConfiguration();
        return $settings->static->protocol . '://'. $settings->static->host . $path;
    }

    public function getUrl($serviceName = "", $remoteEntityId = "", $request = "")
    {
        if (!isset($this->_serviceToControllerMapping[$serviceName])) {
            throw new EngineBlock_Corto_ProxyServer_Exception(
                "Unable to map service '$serviceName' to a controller!"
            );
        }

        $scheme = 'http';
        if (isset($_SERVER['HTTPS'])) {
            $scheme = 'https';
        }

        $host = $_SERVER['HTTP_HOST'];

        $mappedUri = $this->_serviceToControllerMapping[$serviceName];

        $isImplicitVo = false;
        $remoteEntity = false;
        if ($remoteEntityId) {
            $remoteEntity = $this->getRemoteEntity($remoteEntityId);
        }
        if ($remoteEntity && isset($remoteEntity['VoContext'])) {
            if ($request && !isset($request['__'][EngineBlock_Corto_ProxyServer::VO_CONTEXT_PFX])) {
                $isImplicitVo = true;
            }
        }
        if (!$this->_processingMode && $this->_voContext !== null && $serviceName != "spMetadataService" && !$isImplicitVo) {
            $mappedUri .= '/' . "vo:" . $this->_voContext;
        }
        if (!$this->_processingMode && $serviceName !== 'idpMetadataService' && $remoteEntityId) {
            $mappedUri .= '/' . md5($remoteEntityId);
        }

        return $scheme . '://' . $host . $mappedUri;
    }

    public function hasRemoteEntity($entityId)
    {
        return isset($this->_entities['remote'][$entityId]);
    }

    public function getRemoteEntity($entityId)
    {
        if (!isset($this->_entities['remote'][$entityId])) {
            throw new EngineBlock_Corto_ProxyServer_UnknownRemoteEntityException($entityId);
        }
        $entity = $this->_entities['remote'][$entityId];
        $entity['EntityId'] = $entityId;
        return $entity;
    }

    public function getRemoteEntities()
    {
        return $this->_entities['remote'];
    }

    /**
     * Gets current entity by name
     *
     * @param string $name spMetadataService|idpMetadataService
     * @return array mixed
     * @throws EngineBlock_Corto_ProxyServer_UnknownRemoteEntityException
     */
    public function getCurrentEntity($name)
    {
        if (!isset($this->_entities['current'][$name])) {
            throw new EngineBlock_Corto_ProxyServer_UnknownRemoteEntityException($name);
        }
        $entity = $this->_entities['current'][$name];
        return $entity;
    }

    public function getIdpEntityIds()
    {
        $idps = array();
        foreach ($this->_server->getRemoteEntities() as $remoteEntityId => $remoteEntity) {
            if (isset($remoteEntity['SingleSignOnService'])) {
                $idps[] = $remoteEntityId;
            }
        }
        return $idps;
    }

    /**
     * @param array $entities
     */
    public function setCurrentEntities(array $entities)
    {
        $this->_entities['current'] = $entities;
    }

    public function setRemoteEntities($entities)
    {
        $this->_entities['remote'] = $entities;
    }

    public function setTemplateSource($type, $arguments)
    {
        $this->_templateSource = array(
            'type'      => $type,
            'arguments' => $arguments,
        );
        return $this;
    }

    public function getTemplateSource()
    {
        return $this->_templateSource;
    }

//////// MAIN /////////

    public function serve($serviceName, $remoteIdpMd5 = "")
    {
        if (!empty($remoteIdpMd5)) {
            $this->setRemoteIdpMd5($remoteIdpMd5);
        }

        $this->startSession();
        $this->getSessionLog()->info("Started request with parameters: ". var_export(func_get_args(), true));

        $this->getSessionLog()->info("Calling service '$serviceName'");
        $this->getServicesModule()->serve($serviceName);
        $this->getSessionLog()->info("Done calling service '$serviceName'");
    }

    public function setRemoteIdpMd5($remoteIdPMd5)
    {
        $remoteEntityIds = array_keys($this->_entities['remote']);
        foreach ($remoteEntityIds as $remoteEntityId) {
            if (md5($remoteEntityId) === $remoteIdPMd5) {
                $this->_configs['Idp'] = $remoteEntityId;
                $this->_configs['TransparentProxy'] = true;
                $this->getSessionLog()->info("Detected pre-selection of $remoteEntityId as IdP, switching to transparant mode");
                break;
            }
        }
        if (!isset($this->_configs['Idp'])) {
            $this->getSessionLog()->warn("Unable to map remote IdpMD5 '$remoteIdPMd5' to a remote entity!");
        }

        return $this;
    }

////////  REQUEST HANDLING /////////

    public function sendAuthenticationRequest(array $request, $idpEntityId, $scope = null)
    {
        $cookieExpiresStamp = null;
        if (isset($this->_configs['rememberIdp'])) {
            $cookieExpiresStamp = strtotime($this->_configs['rememberIdp']);
        }
        $this->setCookie('selectedIdp', $idpEntityId, $cookieExpiresStamp);

        $originalId = $request['_ID'];

        $newRequest = $this->createEnhancedRequest($request, $idpEntityId, $scope);
        $newId = $newRequest['_ID'];

        // Store the original Request
        $_SESSION[$originalId]['SAMLRequest'] = $request;

        // Store the mapping from the new request ID to the original request ID
        $_SESSION[$newId] = array();
        $_SESSION[$newId]['SAMLRequest'] = $request;
        $_SESSION[$newId]['_InResponseTo'] = $originalId;

        $this->getBindingsModule()->send($newRequest, $this->getRemoteEntity($idpEntityId));
    }

    /**
     *
     *
     * @param string $idp
     * @param array|null $scoping
     * @return array
     */
    public function createEnhancedRequest($originalRequest, $idp, array $scoping = null)
    {
        $remoteMetaData = $this->getRemoteEntity($idp);

        $nameIdPolicy = array('_AllowCreate'  => 'true');
        /**
         * Name policy is not required, so it is only set if configured, SAML 2.0 spec
         * says only following values are allowed:
         *  - urn:oasis:names:tc:SAML:2.0:nameid-format:transient
         *  - urn:oasis:names:tc:SAML:2.0:nameid-format:persistent.
         *
         * Note: Some IDP's like those using ADFS2 do not understand those, for these cases the format can be 'configured as empty
         * or set to an older version.
         */
        // @todo check why it is empty
        if (!empty($remoteMetaData['NameIDFormat'])) {
            $nameIdPolicy['_Format'] = $remoteMetaData['NameIDFormat'];
        }

        $request = array(
            EngineBlock_Corto_XmlToArray::TAG_NAME_PFX       => 'samlp:AuthnRequest',
            EngineBlock_Corto_XmlToArray::PRIVATE_PFX => array(
                'paramname'         => 'SAMLRequest',
                'destinationid'     => $idp,
                'ProtocolBinding'   => $remoteMetaData['SingleSignOnService']['Binding'],
            ),
            '_xmlns:saml'                       => 'urn:oasis:names:tc:SAML:2.0:assertion',
            '_xmlns:samlp'                      => 'urn:oasis:names:tc:SAML:2.0:protocol',

            '_ID'                               => $this->getNewId(),
            '_Version'                          => '2.0',
            '_IssueInstant'                     => $this->timeStamp(),
            '_Destination'                      => $remoteMetaData['SingleSignOnService']['Location'],
            '_ForceAuthn'                       => ($originalRequest['_ForceAuthn']) ? 'true' : 'false',
            '_IsPassive'                        => ($originalRequest['_IsPassive']) ? 'true' : 'false',

            // Send the response to us.
            '_AssertionConsumerServiceURL'      => $this->getUrl('assertionConsumerService'),
            '_ProtocolBinding'                  => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',

            'saml:Issuer' => array('__v' => $this->getUrl('spMetadataService')),
            'ds:Signature' => '__placeholder__',

            'samlp:NameIDPolicy' => $nameIdPolicy
        );

        if (isset($originalRequest['_AttributeConsumingServiceIndex'])) {
            $request['_AttributeConsumingServiceIndex'] = $originalRequest['_AttributeConsumingServiceIndex'];
        }

        if (empty($remoteMetaData['DisableScoping'])) {
            if ($scoping) {
                $scoping = (array) $scoping;
                foreach ($scoping as $scopedIdP) {
                    $request['samlp:Scoping']['samlp:IDPList']['samlp:IDPEntry'][] = array('_ProviderID' => $scoping);
                }
                return $request;
            }

            // Copy original scoping rules
            if (isset($originalRequest['samlp:Scoping'])) {
                $request['samlp:Scoping'] = $originalRequest['samlp:Scoping'];
            }
            else {
                $request['samlp:Scoping'] = array();
            }

            // Decrease or initialize the proxycount
            if (isset($originalRequest['samlp:Scoping']['_ProxyCount'])) {
                $request['samlp:Scoping']['_ProxyCount']--;
            }
            else {
                $request['samlp:Scoping']['_ProxyCount'] = $this->getConfig('max_proxies', 10);
            }

            // Add the issuer of the original request as requester
            if (!isset($request['samlp:Scoping']['samlp:RequesterID'])) {
                $request['samlp:Scoping']['samlp:RequesterID'] = array();
            }
            $request['samlp:Scoping']['samlp:RequesterID'][] = array('__v' => $originalRequest['saml:Issuer']['__v']);
        }

        return $request;
    }

//////// RESPONSE HANDLING ////////

    public function createErrorResponse($request, $errorStatus)
    {
        $response = $this->_createBaseResponse($request);

        $errorCodePrefix = 'urn:oasis:names:tc:SAML:2.0:status:';
        $response['samlp:Status'] = array(
            'samlp:StatusCode' => array(
                '_Value' => 'urn:oasis:names:tc:SAML:2.0:status:Responder',
                'samlp:StatusCode' => array(
                    '_Value' => $errorCodePrefix . $errorStatus,
                ),
            ),
        );
        return $response;
    }

    public function createEnhancedResponse($request, $sourceResponse)
    {
        $response = $this->_createBaseResponse($request);

        // Store the Origin response (from the IdP)
        if (isset($sourceResponse['__']['OriginalResponse'])) {
            $response['__']['OriginalResponse'] = $sourceResponse['__']['OriginalResponse'];
        }
        else {
            $response['__']['OriginalResponse'] = $sourceResponse;
        }

        $inTransparentMode = isset($request[EngineBlock_Corto_XmlToArray::PRIVATE_PFX]['Transparent']) &&
                $request[EngineBlock_Corto_XmlToArray::PRIVATE_PFX]['Transparent'];

        if (isset($sourceResponse['__']['OriginalIssuer'])) {
            $response['__']['OriginalIssuer'] = $sourceResponse['__']['OriginalIssuer'];
        }
        else {
            $response['__']['OriginalIssuer'] = $sourceResponse['saml:Issuer']['__v'];
        }

        if (!$this->isInProcessingMode() && $inTransparentMode) {
            $response['saml:Issuer']['__v']                   = $response['__']['OriginalIssuer'];
            $response['saml:Assertion']['saml:Issuer']['__v'] = $response['__']['OriginalIssuer'];
        }

        if (isset($sourceResponse['_Consent'])) {
            $response['_Consent'] = $sourceResponse['_Consent'];
        }<commit_msg>BACKLOG-915 Patch. Remove '/migrate' during transparent proxy<commit_after><?php

class EngineBlock_Corto_ProxyServer
{
    const ID_PREFIX = 'CORTO';

    const MODULE_BINDINGS   = 'Bindings';
    const MODULE_SERVICES   = 'Services';

    const TEMPLATE_SOURCE_FILESYSTEM = 'filesystem';
    const TEMPLATE_SOURCE_MEMORY     = 'memory';

    const MESSAGE_TYPE_REQUEST  = 'SAMLRequest';
    const MESSAGE_TYPE_RESPONSE = 'SAMLResponse';

    const VO_CONTEXT_PFX          = 'voContext';

    protected $_serviceToControllerMapping = array(
        'singleSignOnService'               => '/authentication/idp/single-sign-on',
        'debugSingleSignOnService'          => '/authentication/sp/debug',
        'continueToIdP'                     => '/authentication/idp/process-wayf',

        'assertionConsumerService'          => '/authentication/sp/consume-assertion',
        'continueToSP'                      => '/authentication/sp/process-consent',
        'provideConsentService'             => '/authentication/idp/provide-consent',
        'processConsentService'             => '/authentication/idp/process-consent',
        'processedAssertionConsumerService' => '/authentication/proxy/processed-assertion',

        'idpMetadataService'                => '/authentication/idp/metadata',
        'spMetadataService'                 => '/authentication/sp/metadata',
    );

    protected $_headers = array();
    protected $_output;

    protected $_voContext = null;

    protected $_requestArray;
    protected $_responseArray;

    protected $_server;
    protected $_systemLog;
    protected $_sessionLog;
    protected $_sessionLogDefault;

    protected $_configs;
    protected $_entities = array(
        'current'=>array(),
        'hosted'=>array(),
        'remote'=>array(),
    );
    protected $_modules = array();
    protected $_templateSource;
    protected $_processingMode = false;

    public function __construct()
    {
        $this->_server = $this;
    }

//////// GETTERS / SETTERS /////////


    public function setVirtualOrganisationContext($voContext)
    {
        $this->_voContext = $voContext;
    }

    public function getVirtualOrganisationContext()
    {
        return $this->_voContext;
    }

    public function getOutput()
    {
        return $this->_output;
    }

    public function getHeaders()
    {
        return $this->_headers;
    }

    public function sendOutput($rawOutput)
    {
        $this->_output = $rawOutput;
    }

    public function sendHeader($name, $value)
    {
        $this->_headers[$name] = $value;
    }

    public function setProcessingMode()
    {
        $this->_processingMode = true;
        return $this;
    }

    public function unsetProcessingMode()
    {
        $this->_processingMode = false;
        return $this;
    }

    public function isInProcessingMode()
    {
        return $this->_processingMode;
    }

    /**
     * Does the request have an "unsolicited" flag
     *
     * @param array $request
     * @return type
     */
    public function isUnsolicitedRequest(array $request)
    {
        return !empty($request[EngineBlock_Corto_XmlToArray::PRIVATE_PFX]['Unsolicited']);
    }

    /**
     * @return EngineBlock_Corto_Module_Bindings
     */
    public function getBindingsModule()
    {
        return $this->_getModule(self::MODULE_BINDINGS);
    }

    /**
     * @param EngineBlock_Corto_Module_Bindings $bindingsInstance
     * @return EngineBlock_Corto_ProxyServer
     */
    public function setBindingsModule(EngineBlock_Corto_Module_Bindings $bindingsInstance)
    {
        return $this->_setModule(self::MODULE_BINDINGS, $bindingsInstance);
    }

    /**
     * @return EngineBlock_Corto_Module_Services
     */
    public function getServicesModule()
    {
        return $this->_getModule(self::MODULE_SERVICES);
    }

    /**
     * @param EngineBlock_Corto_Module_Services $servicesInstance
     * @return EngineBlock_Corto_ProxyServer
     */
    public function setServicesModule(EngineBlock_Corto_Module_Services $servicesInstance)
    {
        return $this->_setModule(self::MODULE_SERVICES, $servicesInstance);
    }

    /**
     * @param string $name
     * @return EngineBlock_Corto_Module_Abstract
     */
    protected function _getModule($name)
    {
        return $this->_modules[$name];
    }

    /**
     * @param  $name
     * @param  $moduleInstance
     * @return EngineBlock_Corto_ProxyServer
     */
    protected function _setModule($name, EngineBlock_Corto_Module_Abstract $moduleInstance)
    {
        $this->_modules[$name] = $moduleInstance;
        return $this;
    }

    public function getConfig($name, $default = null)
    {
        if (isset($this->_configs[$name])) {
            return $this->_configs[$name];
        }
        return $default;
    }

    public function getConfigs()
    {
        return $this->_configs;
    }

    public function setConfig($name, $value)
    {
        $this->_configs[$name] = $value;
        return $this;
    }

    public function setConfigs($configs)
    {
        $this->_configs = $configs;
        return $this;
    }

    public function getAttributeName($attributeId, $ietfLanguageTag = 'en', $fallbackToId = true)
    {
        $metadata = new EngineBlock_Attributes_Metadata();
        return $metadata->getName($attributeId, $ietfLanguageTag, $fallbackToId);
    }

    public function getAttributeDescription($attributeId, $ietfLanguageTag = 'en', $fallbackToId = true)
    {
        $metadata = new EngineBlock_Attributes_Metadata();
        return $metadata->getDescription($attributeId, $ietfLanguageTag);
    }

    /**
     * Return the url of the Static vhost containing media, script and css files
     *
     * @example <?php echo $this->staticUrl(); ?>
     *
     * @return string
     */
    public static function staticUrl($path = "")
    {
        $application = EngineBlock_ApplicationSingleton::getInstance();
        $settings = $application->getConfiguration();
        return $settings->static->protocol . '://'. $settings->static->host . $path;
    }

    public function getUrl($serviceName = "", $remoteEntityId = "", $request = "")
    {
        if (!isset($this->_serviceToControllerMapping[$serviceName])) {
            throw new EngineBlock_Corto_ProxyServer_Exception(
                "Unable to map service '$serviceName' to a controller!"
            );
        }

        $scheme = 'http';
        if (isset($_SERVER['HTTPS'])) {
            $scheme = 'https';
        }

        $host = $_SERVER['HTTP_HOST'];

        $mappedUri = $this->_serviceToControllerMapping[$serviceName];

        $isImplicitVo = false;
        $remoteEntity = false;
        if ($remoteEntityId) {
            $remoteEntity = $this->getRemoteEntity($remoteEntityId);
        }
        if ($remoteEntity && isset($remoteEntity['VoContext'])) {
            if ($request && !isset($request['__'][EngineBlock_Corto_ProxyServer::VO_CONTEXT_PFX])) {
                $isImplicitVo = true;
            }
        }
        if (!$this->_processingMode && $this->_voContext !== null && $serviceName != "spMetadataService" && !$isImplicitVo) {
            $mappedUri .= '/' . "vo:" . $this->_voContext;
        }
        if (!$this->_processingMode && $serviceName !== 'idpMetadataService' && $remoteEntityId) {
            $mappedUri .= '/' . md5($remoteEntityId);
        }

        return $scheme . '://' . $host . $mappedUri;
    }

    public function hasRemoteEntity($entityId)
    {
        return isset($this->_entities['remote'][$entityId]);
    }

    public function getRemoteEntity($entityId)
    {
        if (!isset($this->_entities['remote'][$entityId])) {
            throw new EngineBlock_Corto_ProxyServer_UnknownRemoteEntityException($entityId);
        }
        $entity = $this->_entities['remote'][$entityId];
        $entity['EntityId'] = $entityId;
        return $entity;
    }

    public function getRemoteEntities()
    {
        return $this->_entities['remote'];
    }

    /**
     * Gets current entity by name
     *
     * @param string $name spMetadataService|idpMetadataService
     * @return array mixed
     * @throws EngineBlock_Corto_ProxyServer_UnknownRemoteEntityException
     */
    public function getCurrentEntity($name)
    {
        if (!isset($this->_entities['current'][$name])) {
            throw new EngineBlock_Corto_ProxyServer_UnknownRemoteEntityException($name);
        }
        $entity = $this->_entities['current'][$name];
        return $entity;
    }

    public function getIdpEntityIds()
    {
        $idps = array();
        foreach ($this->_server->getRemoteEntities() as $remoteEntityId => $remoteEntity) {
            if (isset($remoteEntity['SingleSignOnService'])) {
                $idps[] = $remoteEntityId;
            }
        }
        return $idps;
    }

    /**
     * @param array $entities
     */
    public function setCurrentEntities(array $entities)
    {
        $this->_entities['current'] = $entities;
    }

    public function setRemoteEntities($entities)
    {
        $this->_entities['remote'] = $entities;
    }

    public function setTemplateSource($type, $arguments)
    {
        $this->_templateSource = array(
            'type'      => $type,
            'arguments' => $arguments,
        );
        return $this;
    }

    public function getTemplateSource()
    {
        return $this->_templateSource;
    }

//////// MAIN /////////

    public function serve($serviceName, $remoteIdpMd5 = "")
    {
        if (!empty($remoteIdpMd5)) {
            $this->setRemoteIdpMd5($remoteIdpMd5);
        }

        $this->startSession();
        $this->getSessionLog()->info("Started request with parameters: ". var_export(func_get_args(), true));

        $this->getSessionLog()->info("Calling service '$serviceName'");
        $this->getServicesModule()->serve($serviceName);
        $this->getSessionLog()->info("Done calling service '$serviceName'");
    }

    public function setRemoteIdpMd5($remoteIdPMd5)
    {
        $remoteEntityIds = array_keys($this->_entities['remote']);
        foreach ($remoteEntityIds as $remoteEntityId) {
            if (md5($remoteEntityId) === $remoteIdPMd5) {
                $this->_configs['Idp'] = $remoteEntityId;
                $this->_configs['TransparentProxy'] = true;
                $this->getSessionLog()->info("Detected pre-selection of $remoteEntityId as IdP, switching to transparant mode");
                break;
            }
        }
        // Patch Migration BACKLOG-915 Begin
        foreach ($remoteEntityIds as $remoteEntityId) {
            if (substr($remoteEntityId, -8) == "/migrate") {

                if (md5(substr($remoteEntityId, 0, -8)) === $remoteIdPMd5) {
                    $this->_configs['Idp'] = $remoteEntityId;
                    $this->_configs['TransparentProxy'] = true;
                    $this->getSessionLog()->info("Re detected pre-selection of $remoteEntityId as IdP, switching to IdP EntityID with Alias");
                    break;
                }
            }
        }
        // Patch Migration BACKLOG-915 End
        if (!isset($this->_configs['Idp'])) {
            $this->getSessionLog()->warn("Unable to map remote IdpMD5 '$remoteIdPMd5' to a remote entity!");
        }

        return $this;
    }

////////  REQUEST HANDLING /////////

    public function sendAuthenticationRequest(array $request, $idpEntityId, $scope = null)
    {
        $cookieExpiresStamp = null;
        if (isset($this->_configs['rememberIdp'])) {
            $cookieExpiresStamp = strtotime($this->_configs['rememberIdp']);
        }
        $this->setCookie('selectedIdp', $idpEntityId, $cookieExpiresStamp);

        $originalId = $request['_ID'];

        $newRequest = $this->createEnhancedRequest($request, $idpEntityId, $scope);
        $newId = $newRequest['_ID'];

        // Store the original Request
        $_SESSION[$originalId]['SAMLRequest'] = $request;

        // Store the mapping from the new request ID to the original request ID
        $_SESSION[$newId] = array();
        $_SESSION[$newId]['SAMLRequest'] = $request;
        $_SESSION[$newId]['_InResponseTo'] = $originalId;

        $this->getBindingsModule()->send($newRequest, $this->getRemoteEntity($idpEntityId));
    }

    /**
     *
     *
     * @param string $idp
     * @param array|null $scoping
     * @return array
     */
    public function createEnhancedRequest($originalRequest, $idp, array $scoping = null)
    {
        $remoteMetaData = $this->getRemoteEntity($idp);

        $nameIdPolicy = array('_AllowCreate'  => 'true');
        /**
         * Name policy is not required, so it is only set if configured, SAML 2.0 spec
         * says only following values are allowed:
         *  - urn:oasis:names:tc:SAML:2.0:nameid-format:transient
         *  - urn:oasis:names:tc:SAML:2.0:nameid-format:persistent.
         *
         * Note: Some IDP's like those using ADFS2 do not understand those, for these cases the format can be 'configured as empty
         * or set to an older version.
         */
        // @todo check why it is empty
        if (!empty($remoteMetaData['NameIDFormat'])) {
            $nameIdPolicy['_Format'] = $remoteMetaData['NameIDFormat'];
        }

        $request = array(
            EngineBlock_Corto_XmlToArray::TAG_NAME_PFX       => 'samlp:AuthnRequest',
            EngineBlock_Corto_XmlToArray::PRIVATE_PFX => array(
                'paramname'         => 'SAMLRequest',
                'destinationid'     => $idp,
                'ProtocolBinding'   => $remoteMetaData['SingleSignOnService']['Binding'],
            ),
            '_xmlns:saml'                       => 'urn:oasis:names:tc:SAML:2.0:assertion',
            '_xmlns:samlp'                      => 'urn:oasis:names:tc:SAML:2.0:protocol',

            '_ID'                               => $this->getNewId(),
            '_Version'                          => '2.0',
            '_IssueInstant'                     => $this->timeStamp(),
            '_Destination'                      => $remoteMetaData['SingleSignOnService']['Location'],
            '_ForceAuthn'                       => ($originalRequest['_ForceAuthn']) ? 'true' : 'false',
            '_IsPassive'                        => ($originalRequest['_IsPassive']) ? 'true' : 'false',

            // Send the response to us.
            '_AssertionConsumerServiceURL'      => $this->getUrl('assertionConsumerService'),
            '_ProtocolBinding'                  => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',

            'saml:Issuer' => array('__v' => $this->getUrl('spMetadataService')),
            'ds:Signature' => '__placeholder__',

            'samlp:NameIDPolicy' => $nameIdPolicy
        );

        if (isset($originalRequest['_AttributeConsumingServiceIndex'])) {
            $request['_AttributeConsumingServiceIndex'] = $originalRequest['_AttributeConsumingServiceIndex'];
        }

        if (empty($remoteMetaData['DisableScoping'])) {
            if ($scoping) {
                $scoping = (array) $scoping;
                foreach ($scoping as $scopedIdP) {
                    $request['samlp:Scoping']['samlp:IDPList']['samlp:IDPEntry'][] = array('_ProviderID' => $scoping);
                }
                return $request;
            }

            // Copy original scoping rules
            if (isset($originalRequest['samlp:Scoping'])) {
                $request['samlp:Scoping'] = $originalRequest['samlp:Scoping'];
            }
            else {
                $request['samlp:Scoping'] = array();
            }

            // Decrease or initialize the proxycount
            if (isset($originalRequest['samlp:Scoping']['_ProxyCount'])) {
                $request['samlp:Scoping']['_ProxyCount']--;
            }
            else {
                $request['samlp:Scoping']['_ProxyCount'] = $this->getConfig('max_proxies', 10);
            }

            // Add the issuer of the original request as requester
            if (!isset($request['samlp:Scoping']['samlp:RequesterID'])) {
                $request['samlp:Scoping']['samlp:RequesterID'] = array();
            }
            $request['samlp:Scoping']['samlp:RequesterID'][] = array('__v' => $originalRequest['saml:Issuer']['__v']);
        }

        return $request;
    }

//////// RESPONSE HANDLING ////////

    public function createErrorResponse($request, $errorStatus)
    {
        $response = $this->_createBaseResponse($request);

        $errorCodePrefix = 'urn:oasis:names:tc:SAML:2.0:status:';
        $response['samlp:Status'] = array(
            'samlp:StatusCode' => array(
                '_Value' => 'urn:oasis:names:tc:SAML:2.0:status:Responder',
                'samlp:StatusCode' => array(
                    '_Value' => $errorCodePrefix . $errorStatus,
                ),
            ),
        );
        return $response;
    }

    public function createEnhancedResponse($request, $sourceResponse)
    {
        $response = $this->_createBaseResponse($request);

        // Store the Origin response (from the IdP)
        if (isset($sourceResponse['__']['OriginalResponse'])) {
            $response['__']['OriginalResponse'] = $sourceResponse['__']['OriginalResponse'];
        }
        else {
            $response['__']['OriginalResponse'] = $sourceResponse;
        }

        $inTransparentMode = isset($request[EngineBlock_Corto_XmlToArray::PRIVATE_PFX]['Transparent']) &&
                $request[EngineBlock_Corto_XmlToArray::PRIVATE_PFX]['Transparent'];

        if (isset($sourceResponse['__']['OriginalIssuer'])) {
            $response['__']['OriginalIssuer'] = $sourceResponse['__']['OriginalIssuer'];
        }
        else {
            $response['__']['OriginalIssuer'] = $sourceResponse['saml:Issuer']['__v'];
        }

        if (!$this->isInProcessingMode() && $inTransparentMode) {
            // Patch Migration BACKLOG-915 Begin
            if (substr($response['__']['OriginalIssuer'],-8) == '/migrate') {
                $response['__']['OriginalIssuer'] = substr($response['__']['OriginalIssuer'],0,-8);
            }
            // Patch Migration BACKLOG-915 End
            $response['saml:Issuer']['__v']                   = $response['__']['OriginalIssuer'];
            $response['saml:Assertion']['saml:Issuer']['__v'] = $response['__']['OriginalIssuer'];
        }

        if (isset($sourceResponse['_Consent'])) {
            $response['_Consent'] = $sourceResponse['_Consent'];
        }
{'fraction_non_alphanumeric': 0.13867279421942913, 'fraction_numerical': 0.002408571191273774, 'mean_word_length': 2.327376565954311, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 257, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 4}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4140503', 'n_tokens_mistral': 10282, 'n_tokens_neox': 10061, 'n_words': 1748}