/**************************************************************
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF 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.
 *
 *************************************************************/



// MARKER(update_precomp.py): autogen include statement, do not remove
#include "precompiled_webdav.hxx"

#include "CurlRequest.hxx"

#include <string.h>

using namespace ::com::sun::star;
using namespace http_dav_ucp;

CurlRequest::CurlRequest( CURL *curl )
    : curl( curl )
    , curlUrl( NULL )
    , requestHeaders( NULL )
    , requestBody( NULL )
    , requestBodyOffset( 0 )
    , requestBodySize( 0 )
    , useChunkedEncoding( false )
    , provideCredentialsCallback( NULL )
    , provideCredentialsUserdata( NULL )
    , statusCode( 0 )
    , responseBodyInputStream( new CurlInputStream() )
{
    curl_easy_setopt( curl, CURLOPT_HEADERFUNCTION, Curl_HeaderReceived );
    curl_easy_setopt( curl, CURLOPT_HEADERDATA, this );
    curl_easy_setopt( curl, CURLOPT_WRITEFUNCTION, Curl_MoreBodyReceived );
    curl_easy_setopt( curl, CURLOPT_WRITEDATA, this );
}

CurlRequest::~CurlRequest()
{
    if ( curlUrl != NULL )
        curl_url_cleanup( curlUrl );
    curl_easy_setopt( curl, CURLOPT_CURLU, NULL );

    curl_easy_setopt( curl, CURLOPT_HTTPHEADER, NULL );
    if ( requestHeaders != NULL )
        curl_slist_free_all( requestHeaders );
    curl_easy_setopt( curl, CURLOPT_READFUNCTION, NULL );
    curl_easy_setopt( curl, CURLOPT_READDATA, NULL );
    curl_easy_setopt( curl, CURLOPT_INFILESIZE, -1 );
    curl_easy_setopt( curl, CURLOPT_SEEKFUNCTION, NULL );
    curl_easy_setopt( curl, CURLOPT_SEEKDATA, NULL );
    curl_easy_setopt( curl, CURLOPT_HEADERFUNCTION, NULL );
    curl_easy_setopt( curl, CURLOPT_HEADERDATA, NULL );
    curl_easy_setopt( curl, CURLOPT_CUSTOMREQUEST, NULL );
    curl_easy_setopt( curl, CURLOPT_WRITEFUNCTION, NULL );
    curl_easy_setopt( curl, CURLOPT_WRITEDATA, NULL );
}

void CurlRequest::addHeader( const rtl::OString &name, const rtl::OString &value) throw (DAVException)
{
    rtl::OString line = name + ": " + value;
    struct curl_slist *appended = curl_slist_append( requestHeaders, line.getStr() );
    if ( appended != NULL )
    {
        requestHeaders = appended;
        curl_easy_setopt( curl, CURLOPT_HTTPHEADER, requestHeaders );
    }
    else
        throw DAVException( DAVException::DAV_SESSION_CREATE, rtl::OUString::createFromAscii( "Out of memory" ) );
}

void CurlRequest::setRequestBody( const char *body, size_t size )
{
    requestBody = body;
    requestBodyOffset = 0;
    requestBodySize = size;

    curl_easy_setopt( curl, CURLOPT_READFUNCTION, Curl_SendMoreBody );
    curl_easy_setopt( curl, CURLOPT_READDATA, this );
    curl_easy_setopt( curl, CURLOPT_SEEKFUNCTION, Curl_SeekCallback );
    curl_easy_setopt( curl, CURLOPT_SEEKDATA, this );
    if ( useChunkedEncoding )
        addHeader( "Transfer-Encoding", "chunked" );
    else
        curl_easy_setopt( curl, CURLOPT_INFILESIZE_LARGE, (curl_off_t)size );
}

int CurlRequest::Curl_SeekCallback( void *userdata, curl_off_t offset, int origin )
{
    CurlRequest *request = static_cast< CurlRequest* >( userdata );
    if ( origin == SEEK_SET )
        request->requestBodyOffset = (size_t) offset;
    else if ( origin == SEEK_CUR )
        request->requestBodyOffset += (size_t) offset;
    else if ( origin == SEEK_END )
        request->requestBodyOffset += (size_t) ((curl_off_t)request->requestBodySize + offset);
    else
        return CURL_SEEKFUNC_CANTSEEK;
    if ( request->requestBodyOffset > request->requestBodySize )
        request->requestBodyOffset = request->requestBodySize;
    return CURL_SEEKFUNC_OK;
}

size_t CurlRequest::Curl_SendMoreBody( char *buffer, size_t size, size_t nitems, void *userdata )
{
    CurlRequest *request = static_cast< CurlRequest* >( userdata );
    OSL_TRACE("Curl_SendMoreBody");
    return request->curlSendMoreBody( buffer, nitems );
}

size_t CurlRequest::curlSendMoreBody( char *buffer, size_t len )
{
    size_t bytesToSend = requestBodySize - requestBodyOffset;
    if ( bytesToSend > len )
        bytesToSend = len;
    memcpy( buffer, requestBody, bytesToSend );
    requestBodyOffset += bytesToSend;
    return bytesToSend;
}

void CurlRequest::setProvideCredentialsCallback( bool (*callback)(long statusCode, void *userdata) throw (DAVException), void *userdata )
{
    provideCredentialsCallback = callback;
    provideCredentialsUserdata = userdata;
}

void CurlRequest::setURI( CurlUri uri, rtl::OUString path ) throw (DAVException)
{
    if ( curlUrl != NULL )
    {
        curl_url_cleanup( curlUrl );
        curlUrl = NULL;
    }

    curlUrl = curl_url();
    if ( curlUrl == NULL )
        throw DAVException( DAVException::DAV_SESSION_CREATE, rtl::OUString::createFromAscii( "Out of memory" ) );

    if ( CurlUri( path ).GetHost().isEmpty() )
    {
        // "path" is really a path, not a URI
        curl_url_set( curlUrl, CURLUPART_URL, rtl::OUStringToOString( uri.GetURI(), RTL_TEXTENCODING_UTF8 ).getStr(), 0 );
        curl_url_set( curlUrl, CURLUPART_PATH, rtl::OUStringToOString( path, RTL_TEXTENCODING_UTF8 ).getStr(), 0 );
    }
    else
    {
        // The "path" is a full URI
        curl_url_set( curlUrl, CURLUPART_URL, rtl::OUStringToOString( path, RTL_TEXTENCODING_UTF8 ).getStr(), 0 );
    }
    curl_easy_setopt( curl, CURLOPT_CURLU, curlUrl );
}

CURLcode CurlRequest::copy( CurlUri uri, rtl::OUString path ) throw(DAVException)
{
    setURI( uri, path );
    curl_easy_setopt( curl, CURLOPT_HTTPGET, 1L );
    curl_easy_setopt( curl, CURLOPT_CUSTOMREQUEST, "COPY" );
    return perform();
}

CURLcode CurlRequest::delete_( CurlUri uri, rtl::OUString path ) throw (DAVException)
{
    setURI( uri, path );
    curl_easy_setopt( curl, CURLOPT_HTTPGET, 1L );
    curl_easy_setopt( curl, CURLOPT_CUSTOMREQUEST, "DELETE" );
    return perform();
}

CURLcode CurlRequest::get( CurlUri uri, rtl::OUString path ) throw(DAVException)
{
    setURI( uri, path );
    curl_easy_setopt( curl, CURLOPT_HTTPGET, 1L );
    curl_easy_setopt( curl, CURLOPT_CUSTOMREQUEST, "GET" );
    return perform();
}

CURLcode CurlRequest::head( CurlUri uri, rtl::OUString path ) throw (DAVException)
{
    setURI( uri, path );
    curl_easy_setopt( curl, CURLOPT_NOBODY, 1L );
    curl_easy_setopt( curl, CURLOPT_CUSTOMREQUEST, "HEAD" );
    return perform();
}

CURLcode CurlRequest::lock( CurlUri uri, rtl::OUString path ) throw (DAVException)
{
    setURI( uri, path );
    curl_easy_setopt( curl, CURLOPT_UPLOAD, 1L );
    curl_easy_setopt( curl, CURLOPT_CUSTOMREQUEST, "LOCK" );
    return perform();
}

CURLcode CurlRequest::mkcol( CurlUri uri, rtl::OUString path ) throw (DAVException)
{
    setURI( uri, path );
    curl_easy_setopt( curl, CURLOPT_HTTPGET, 1L );
    curl_easy_setopt( curl, CURLOPT_CUSTOMREQUEST, "MKCOL" );
    return perform();
}

CURLcode CurlRequest::move( CurlUri uri, rtl::OUString path ) throw (DAVException)
{
    setURI( uri, path );
    curl_easy_setopt( curl, CURLOPT_HTTPGET, 1L );
    curl_easy_setopt( curl, CURLOPT_CUSTOMREQUEST, "MOVE" );
    return perform();
}

CURLcode CurlRequest::post( CurlUri uri, rtl::OUString path ) throw (DAVException)
{
    setURI( uri, path );
    curl_easy_setopt( curl, CURLOPT_UPLOAD, 1L );
    curl_easy_setopt( curl, CURLOPT_CUSTOMREQUEST, "POST" );
    return perform();
}

CURLcode CurlRequest::propfind( CurlUri uri, rtl::OUString path ) throw (DAVException)
{
    setURI( uri, path );
    curl_easy_setopt( curl, CURLOPT_UPLOAD, 1L );
    curl_easy_setopt( curl, CURLOPT_CUSTOMREQUEST, "PROPFIND" );
    return perform();
}

CURLcode CurlRequest::proppatch( CurlUri uri, rtl::OUString path ) throw (DAVException)
{
    setURI( uri, path );
    curl_easy_setopt( curl, CURLOPT_UPLOAD, 1L );
    curl_easy_setopt( curl, CURLOPT_CUSTOMREQUEST, "PROPPATCH" );
    return perform();
}

CURLcode CurlRequest::put( CurlUri uri, rtl::OUString path ) throw (DAVException)
{
    setURI( uri, path );
    curl_easy_setopt( curl, CURLOPT_UPLOAD, 1L );
    curl_easy_setopt( curl, CURLOPT_CUSTOMREQUEST, "PUT" );
    return perform();
}

CURLcode CurlRequest::unlock( CurlUri uri, rtl::OUString path ) throw (DAVException)
{
    setURI( uri, path );
    curl_easy_setopt( curl, CURLOPT_HTTPGET, 1L );
    curl_easy_setopt( curl, CURLOPT_CUSTOMREQUEST, "UNLOCK" );
    return perform();
}

CURLcode CurlRequest::perform() throw (DAVException)
{
    CURLcode rc = curl_easy_perform( curl );
    long statusCode = 0;
    curl_easy_getinfo( curl, CURLINFO_RESPONSE_CODE, &statusCode );
    if ( ( statusCode == 401 || statusCode == 407 ) && provideCredentialsCallback != NULL )
    {
        bool haveCredentials = provideCredentialsCallback( statusCode, provideCredentialsUserdata );
        if ( haveCredentials )
        {
            // rewind body:
            requestBodyOffset = 0;
            // retry with credentials:
            rc = curl_easy_perform( curl );
            // If this was to authenticate with the proxy, we may need to authenticate
            // with the destination host too:
            if ( statusCode == 407 )
            {
                curl_easy_getinfo( curl, CURLINFO_RESPONSE_CODE, &statusCode );
                if ( statusCode == 401 && provideCredentialsCallback != NULL )
                {
                    haveCredentials = provideCredentialsCallback( statusCode, provideCredentialsUserdata );
                    if ( haveCredentials )
                    {
                        // rewind body:
                        requestBodyOffset = 0;
                        // retry with credentials:
                        rc = curl_easy_perform( curl );
                    }
                }
            }
        }
    }
    return rc;
}

size_t CurlRequest::Curl_HeaderReceived( char *buffer, size_t size, size_t nitems, void *userdata )
{
    CurlRequest *request = static_cast< CurlRequest* >( userdata );
    OSL_TRACE("Curl_HeaderReceived");
    request->curlHeaderReceived( buffer, nitems );
    return nitems;
}

void CurlRequest::curlHeaderReceived( const char *buffer, size_t size )
{
    rtl::OString lineCrLf( buffer, size );
    sal_Int32 cr = lineCrLf.indexOf( "\r" );
    if ( cr < 0 )
        return;

    rtl::OString line = lineCrLf.copy( 0, cr );
    if ( line.indexOf( "HTTP/" ) == 0 )
    {
        // Status line
        // Throw away any response headers from a prior response:
        responseHeaders.clear();
        sal_Int32 idxFirstSpace = line.indexOf( ' ' );
        if ( idxFirstSpace > 0 )
        {
            int idxSecondSpace = line.indexOf( ' ', idxFirstSpace + 1 );
            if ( idxSecondSpace > 0 )
            {
                reasonPhrase = line.copy( idxSecondSpace + 1 );
                statusCode = line.copy( idxFirstSpace + 1, idxSecondSpace - idxFirstSpace - 1 ).toInt32();
            }
            else
            {
                reasonPhrase = "";
                statusCode = line.copy( idxFirstSpace + 1 ).toInt32();
            }
        }
    }
    else
    {
        // Header line
        if ( line.getLength() == 0 )
        {
            // End of HTTP header
            // Discard any intermediate body from 100 Trying or 401 Authentication required:
            responseBodyInputStream = new CurlInputStream();
            return;
        }
        sal_Int32 colon = line.indexOf( ':' );
        if ( colon < 0 )
        {
            OSL_TRACE("Non-empty HTTP line without a ':'. Folded header deprecated by RFC 7230?");
            return;
        }
        Header header;
        header.name = line.copy( 0, colon ).toAsciiLowerCase();
        header.value = line.copy( colon + 1 ).trim();
        responseHeaders.push_back(header);
    }
}

const CurlRequest::Header *CurlRequest::findResponseHeader( const rtl::OString &name )
{
    std::vector< CurlRequest::Header >::const_iterator it( responseHeaders.begin() );
    const std::vector< CurlRequest::Header >::const_iterator end( responseHeaders.end() );
    for ( ; it != end; it++ )
    {
        if ( name.equalsIgnoreAsciiCase( it->name ) )
            return &(*it);
    }
    return NULL;
}

void CurlRequest::saveResponseBodyTo( const uno::Reference< io::XOutputStream > & xOutStream)
{
    xOutputStream = xOutStream;
}

size_t CurlRequest::Curl_MoreBodyReceived( char *buffer, size_t size, size_t nitems, void *userdata )
{
    CurlRequest *request = static_cast< CurlRequest* >( userdata );
    request->curlMoreBodyReceived( buffer, nitems );
    return nitems;
}

void CurlRequest::curlMoreBodyReceived( const char *buffer, size_t size )
{
    if ( xOutputStream.is() )
    {
        const uno::Sequence< sal_Int8 > aDataSeq( (sal_Int8 *)buffer, size );
        xOutputStream->writeBytes( aDataSeq );
    }
    else if ( responseBodyInputStream.is() )
        responseBodyInputStream->AddToStream( buffer, size );
}
