/**
 * Licensed to the University Corporation for Advanced Internet
 * Development, Inc. (UCAID) under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for
 * additional information regarding copyright ownership.
 *
 * UCAID licenses this file to you under the Apache License,
 * Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the
 * License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific
 * language governing permissions and limitations under the License.
 */

/**
 * SAML2ECPEncoder.cpp
 * 
 * SAML 2.0 ECP profile message encoder.
 */

#include "internal.h"
#include "exceptions.h"
#include "signature/ContentReference.h"
#include "saml1/core/Protocols.h"
#include "saml2/binding/SAML2MessageEncoder.h"
#include "saml2/core/Protocols.h"

#include <sstream>
#include <xmltooling/logging.h>
#include <xmltooling/impl/AnyElement.h>
#include <xmltooling/io/HTTPResponse.h>
#include <xmltooling/util/NDC.h>
#include <xmltooling/signature/Signature.h>
#include <xmltooling/soap/SOAP.h>

using namespace samlconstants;
using namespace opensaml::saml2p;
using namespace opensaml::saml2md;
using namespace opensaml;
using namespace xmlconstants;
using namespace xmlsignature;
using namespace soap11;
using namespace xmltooling::logging;
using namespace xmltooling;
using namespace std;

using boost::scoped_ptr;

namespace opensaml {
    namespace saml2p {              
        
        static const XMLCh ProviderName[] = UNICODE_LITERAL_12(P, r, o, v, i, d, e, r, N, a, m, e);

        class SAML_DLLLOCAL SAML2ECPEncoder : public SAML2MessageEncoder
        {
        public:
            SAML2ECPEncoder(const DOMElement* e);
            virtual ~SAML2ECPEncoder() {}

            long encode(
                GenericResponse& genericResponse,
                XMLObject* xmlObject,
                const char* destination,
                const EntityDescriptor* recipient=nullptr,
                const char* relayState=nullptr,
                const ArtifactGenerator* artifactGenerator=nullptr,
                const Credential* credential=nullptr,
                const XMLCh* signatureAlg=nullptr,
                const XMLCh* digestAlg=nullptr
                ) const;
            
        private:
            auto_ptr_XMLCh m_actor;
            const XMLCh* m_providerName;
            scoped_ptr<IDPList> m_idpList;
            AnyElementBuilder m_anyBuilder;
        };

        MessageEncoder* SAML_DLLLOCAL SAML2ECPEncoderFactory(const DOMElement* const & e, bool)
        {
            return new SAML2ECPEncoder(e);
        }
    };
};

SAML2ECPEncoder::SAML2ECPEncoder(const DOMElement* e)
        : m_actor("http://schemas.xmlsoap.org/soap/actor/next"), m_providerName(nullptr) {

    // Fishy alert: we ignore the namespace and look for a matching DOM Attr node by name only.
    // Can't use DOM 1 calls, so we have to walk the attribute list by hand.

    const DOMNamedNodeMap* attributes = e ? e->getAttributes() : nullptr;
    XMLSize_t size = attributes ? attributes->getLength() : 0;
    for (XMLSize_t i = 0; i < size; ++i) {
        const DOMNode* attr = attributes->item(i);
        if (XMLString::equals(attr->getLocalName(), ProviderName)) {
            m_providerName = attr->getNodeValue();
        }
    }

    DOMElement* child = e ? XMLHelper::getFirstChildElement(e, SAML20P_NS, IDPList::LOCAL_NAME) : nullptr;
    if (child)
        m_idpList.reset(dynamic_cast<IDPList*>(XMLObjectBuilder::buildOneFromElement(child)));
}

long SAML2ECPEncoder::encode(
    GenericResponse& genericResponse,
    XMLObject* xmlObject,
    const char*,
    const EntityDescriptor*,
    const char* relayState,
    const ArtifactGenerator*,
    const Credential* credential,
    const XMLCh* signatureAlg,
    const XMLCh* digestAlg
    ) const
{
#ifdef _DEBUG
    xmltooling::NDC ndc("encode");
#endif
    Category& log = Category::getInstance(SAML_LOGCAT ".MessageEncoder.SAML2ECP");

    log.debug("validating input");
    if (xmlObject->getParent())
        throw BindingException("Cannot encode XML content with parent.");

    Response* response = nullptr;
    AuthnRequest* request = dynamic_cast<AuthnRequest*>(xmlObject);
    if (!request) {
        response = dynamic_cast<Response*>(xmlObject);
        if (!response)
            throw BindingException("XML content for SAML 2.0 ECP Encoder must be a SAML 2.0 AuthnRequest or Response.");
    }
    
    if (request && !request->getAssertionConsumerServiceURL())
        throw BindingException("AuthnRequest must carry an AssertionConsumerServiceURL by value.");
    else if (response && !response->getDestination())
        throw BindingException("Response must carry a Destination attribute.");
    
    // PAOS request leg is a custom MIME type, SOAP response leg is just text/xml.
    genericResponse.setContentType(request ? "application/vnd.paos+xml" : "text/xml");
    HTTPResponse* httpResponse = dynamic_cast<HTTPResponse*>(&genericResponse);
    if (httpResponse) {
        httpResponse->setResponseHeader("Expires", "01-Jan-1997 12:00:00 GMT");
        httpResponse->setResponseHeader("Cache-Control", "no-cache, no-store, must-revalidate, private");
        httpResponse->setResponseHeader("Pragma", "no-cache");
        if (request) {
            preserveCorrelationID(*httpResponse, *request, relayState);
        }
    }

    // Wrap it in a SOAP envelope.
    Envelope* env = EnvelopeBuilder::buildEnvelope();
    Header* header = HeaderBuilder::buildHeader();
    env->setHeader(header);
    Body* body = BodyBuilder::buildBody();
    env->setBody(body);
    body->getUnknownXMLObjects().push_back(xmlObject);

    ElementProxy* hdrblock;
    xmltooling::QName qMU(SOAP11ENV_NS, Header::MUSTUNDERSTAND_ATTRIB_NAME, SOAP11ENV_PREFIX);
    xmltooling::QName qActor(SOAP11ENV_NS, Header::ACTOR_ATTRIB_NAME, SOAP11ENV_PREFIX);
    
    if (request) {
        // Create paos:Request header.
        static const XMLCh service[] = UNICODE_LITERAL_7(s,e,r,v,i,c,e);
        static const XMLCh responseConsumerURL[] = UNICODE_LITERAL_19(r,e,s,p,o,n,s,e,C,o,n,s,u,m,e,r,U,R,L);
        hdrblock = dynamic_cast<ElementProxy*>(m_anyBuilder.buildObject(PAOS_NS, saml1p::Request::LOCAL_NAME, PAOS_PREFIX));
        hdrblock->setAttribute(qMU, XML_ONE);
        hdrblock->setAttribute(qActor, m_actor.get());
        hdrblock->setAttribute(xmltooling::QName(nullptr, service), SAML20ECP_NS);
        hdrblock->setAttribute(xmltooling::QName(nullptr, responseConsumerURL), request->getAssertionConsumerServiceURL());
        header->getUnknownXMLObjects().push_back(hdrblock);

        // Create ecp:Request header.
        static const XMLCh IsPassive[] = UNICODE_LITERAL_9(I,s,P,a,s,s,i,v,e);
        hdrblock = dynamic_cast<ElementProxy*>(m_anyBuilder.buildObject(SAML20ECP_NS, saml1p::Request::LOCAL_NAME, SAML20ECP_PREFIX));
        hdrblock->setAttribute(qMU, XML_ONE);
        hdrblock->setAttribute(qActor, m_actor.get());
        if (!request->IsPassive())
            hdrblock->setAttribute(xmltooling::QName(nullptr,IsPassive), XML_ZERO);
        if (m_providerName)
            hdrblock->setAttribute(xmltooling::QName(nullptr,ProviderName), m_providerName);
        hdrblock->getUnknownXMLObjects().push_back(request->getIssuer()->clone());
        if (request->getScoping() && request->getScoping()->getIDPList())
            hdrblock->getUnknownXMLObjects().push_back(request->getScoping()->getIDPList()->clone());
        else if (m_idpList.get())
            hdrblock->getUnknownXMLObjects().push_back(m_idpList->clone());
        header->getUnknownXMLObjects().push_back(hdrblock);
    }
    else {
        // Create ecp:Response header.
        hdrblock = dynamic_cast<ElementProxy*>(m_anyBuilder.buildObject(SAML20ECP_NS, Response::LOCAL_NAME, SAML20ECP_PREFIX));
        hdrblock->setAttribute(qMU, XML_ONE);
        hdrblock->setAttribute(qActor, m_actor.get());
        hdrblock->setAttribute(xmltooling::QName(nullptr,AuthnRequest::ASSERTIONCONSUMERSERVICEURL_ATTRIB_NAME), response->getDestination());
        header->getUnknownXMLObjects().push_back(hdrblock);
    }
    
    if (relayState && *relayState) {
        // Create ecp:RelayState header.
        static const XMLCh RelayState[] = UNICODE_LITERAL_10(R,e,l,a,y,S,t,a,t,e);
        hdrblock = dynamic_cast<ElementProxy*>(m_anyBuilder.buildObject(SAML20ECP_NS, RelayState, SAML20ECP_PREFIX));
        hdrblock->setAttribute(qMU, XML_ONE);
        hdrblock->setAttribute(qActor, m_actor.get());
        auto_ptr_XMLCh rs(relayState);
        hdrblock->setTextContent(rs.get());
        header->getUnknownXMLObjects().push_back(hdrblock);
    }
    
    try {
        DOMElement* rootElement = nullptr;
        if (credential) {
            if (request->getSignature()) {
                log.debug("message already signed, skipping signature operation");
                rootElement = env->marshall();
            }
            else {
                log.debug("signing the message and marshalling the envelope");
    
                // Build a Signature.
                Signature* sig = SignatureBuilder::buildSignature();
                request->setSignature(sig);    
                if (signatureAlg)
                    sig->setSignatureAlgorithm(signatureAlg);
                if (digestAlg) {
                    opensaml::ContentReference* cr = dynamic_cast<opensaml::ContentReference*>(sig->getContentReference());
                    if (cr)
                        cr->setDigestAlgorithm(digestAlg);
                }
        
                // Sign message while marshalling.
                vector<Signature*> sigs(1,sig);
                rootElement = env->marshall((DOMDocument*)nullptr,&sigs,credential);
            }
        }
        else {
            log.debug("marshalling the envelope");
            rootElement = env->marshall();
        }

        stringstream s;
        s << *rootElement;
        
        if (log.isDebugEnabled()) {
            string forlog(s.str());
            log.debug("marshalled envelope:\n%s", forlog.c_str());
        }

        log.debug("sending serialized envelope");
        long ret = genericResponse.sendResponse(s);
    
        // Cleanup by destroying XML.
        delete env;
        return ret;
    }
    catch (XMLToolingException&) {
        // A bit weird...we have to "revert" things so that the message is isolated
        // so the caller can free it.
        xmlObject->getParent()->detach();
        xmlObject->detach();
        throw;
    }
}
