/*
 * multistatus.c : parse multistatus (error) responses.
 *
 * ====================================================================
 *    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.
 * ====================================================================
 */



#include <assert.h>

#include <apr.h>

#include <serf.h>
#include <serf_bucket_types.h>

#include "svn_private_config.h"
#include "svn_hash.h"
#include "svn_dirent_uri.h"
#include "svn_path.h"
#include "svn_string.h"
#include "svn_xml.h"
#include "svn_props.h"
#include "svn_dirent_uri.h"

#include "private/svn_dep_compat.h"
#include "private/svn_fspath.h"

#include "ra_serf.h"

/* The current state of our XML parsing. */
typedef enum iprops_state_e {
  INITIAL = XML_STATE_INITIAL,
  MS_MULTISTATUS,

  MS_RESPONSE,
  MS_RESPONSE_HREF,

  MS_PROPSTAT,
  MS_PROPSTAT_PROP,
  MS_PROPSTAT_PROP_NAME,
  MS_PROPSTAT_STATUS,
  MS_PROPSTAT_RESPONSEDESCRIPTION,
  MS_PROPSTAT_ERROR,
  MS_PROPSTAT_ERROR_HUMANREADABLE,

  MS_RESPONSE_STATUS,
  MS_RESPONSE_RESPONSEDESCRIPTION,
  MS_RESPONSE_ERROR,
  MS_RESPONSE_ERROR_HUMANREADABLE,

  MS_MULTISTATUS_RESPONSEDESCRIPTION,

  D_ERROR,
  S_ERROR,
  M_ERROR_HUMANREADABLE
} iprops_state_e;

/*
  <D:multistatus xmlns:D="DAV:">
    <D:response>
      <D:href>http://something</D:href>
      <!-- Possibly multiple D:href elements -->
      <D:status>HTTP/1.1 500 failed</D:status>
      <D:error>
        <S:human-readable code="12345">
          Some Subversion error
        </S:human-readable>
      </D:error>
      <D:responsedescription>
        Human readable description
      </D:responsedescription>
      <D:location>http://redirected</D:location>
    </D:response>
    ...
  </D:multistatus>

  Or for property operations:

  <D:multistatus xmlns:D="DAV:">
    <D:response>
      <D:href>http://somewhere-else</D:href>
      <D:propstat>
        <D:propname><C:myprop /></D:propname>
        <D:status>HTTP/1.1 499 failed</D:status>
        <D:error>
          <S:human-readable code="12345">
            Some Subversion error
          </S:human-readable>
        </D:error>
        <D:responsedescription>
          Human readable description
        </D:responsedescription>
      </D:propstat>
      <D:status>HTTP/1.1 499 failed</D:status>
      <D:error>
        <S:human-readable code="12345">
          Some Subversion error
        </S:human-readable>
      </D:error>
      <D:responsedescription>
        Human readable description
      </D:responsedescription>
      <D:location>http://redirected</D:location>
    <D:responsedescription>
      Global description
    <D:responsedescription>
  </D:multistatus>

  Or on request failures
  <D:error>
    <X:some-error xmlns="QQ" />
    <D:human-readable code="12345">
          Some Subversion error
    </D:human-readable>
  </D:error>
 */

#define D_ "DAV:"
#define S_ SVN_XML_NAMESPACE
#define M_ "http://apache.org/dav/xmlns"
static const svn_ra_serf__xml_transition_t multistatus_ttable[] = {
  { INITIAL, D_, "multistatus", MS_MULTISTATUS,
    FALSE, { NULL }, FALSE },

  { MS_MULTISTATUS, D_, "responsedescription", MS_MULTISTATUS_RESPONSEDESCRIPTION,
    TRUE, { NULL }, TRUE },

  /* <response> */
  { MS_MULTISTATUS, D_, "response", MS_RESPONSE,
    FALSE, { NULL }, TRUE },

  { MS_RESPONSE, D_, "href", MS_RESPONSE_HREF,
    TRUE, { NULL }, TRUE },

  /* <propstat> */
  { MS_RESPONSE, D_, "propstat", MS_PROPSTAT,
    FALSE, { NULL }, TRUE },

  { MS_PROPSTAT, D_, "prop", MS_PROPSTAT_PROP,
    FALSE, { NULL }, FALSE },

  { MS_PROPSTAT_PROP, "", "*", MS_PROPSTAT_PROP_NAME,
    FALSE, { NULL }, FALSE },

  { MS_PROPSTAT, D_, "status", MS_PROPSTAT_STATUS,
    TRUE, { NULL }, TRUE },

  { MS_PROPSTAT, D_, "responsedescription", MS_PROPSTAT_RESPONSEDESCRIPTION,
    TRUE, { NULL }, TRUE },

  { MS_PROPSTAT, D_, "error", MS_PROPSTAT_ERROR,
    FALSE, { NULL }, FALSE },

  { MS_PROPSTAT_ERROR, M_, "human-readable", MS_PROPSTAT_ERROR_HUMANREADABLE,
    TRUE, { "?errcode", NULL }, TRUE },
  /* </propstat> */


  { MS_RESPONSE, D_, "status", MS_RESPONSE_STATUS,
    TRUE, { NULL }, TRUE },

  { MS_RESPONSE, D_, "responsedescription", MS_RESPONSE_RESPONSEDESCRIPTION,
    TRUE, { NULL }, TRUE },

  { MS_RESPONSE, D_, "error", MS_RESPONSE_ERROR,
    FALSE, { NULL }, TRUE },

  { MS_RESPONSE_ERROR, M_, "human-readable", MS_RESPONSE_ERROR_HUMANREADABLE,
    TRUE, { "?errcode", NULL }, TRUE },

  /* </response> */

  { MS_MULTISTATUS, D_, "responsedescription", MS_MULTISTATUS_RESPONSEDESCRIPTION,
    TRUE, { NULL }, TRUE },


  { INITIAL, D_, "error", D_ERROR,
    FALSE, { NULL }, TRUE },

  { D_ERROR, S_, "error", S_ERROR,
    FALSE, { NULL }, FALSE },

  { D_ERROR, M_, "human-readable", M_ERROR_HUMANREADABLE,
    TRUE, { "?errcode", NULL }, TRUE },

  { 0 }
};

/* Given a string like "HTTP/1.1 500 (status)" in BUF, parse out the numeric
   status code into *STATUS_CODE_OUT.  Ignores leading whitespace. */
static svn_error_t *
parse_status_line(int *status_code_out,
                  const char **reason,
                  const char *status_line,
                  apr_pool_t *result_pool,
                  apr_pool_t *scratch_pool)
{
  svn_error_t *err;
  const char *token;
  char *tok_status;
  svn_stringbuf_t *temp_buf = svn_stringbuf_create(status_line, scratch_pool);

  svn_stringbuf_strip_whitespace(temp_buf);
  token = apr_strtok(temp_buf->data, " \t\r\n", &tok_status);
  if (token)
    token = apr_strtok(NULL, " \t\r\n", &tok_status);
  if (!token)
    return svn_error_createf(SVN_ERR_RA_DAV_MALFORMED_DATA, NULL,
                             _("Malformed DAV:status '%s'"),
                             status_line);
  err = svn_cstring_atoi(status_code_out, token);
  if (err)
    return svn_error_createf(SVN_ERR_RA_DAV_MALFORMED_DATA, err,
                             _("Malformed DAV:status '%s'"),
                             status_line);

  token = apr_strtok(NULL, " \t\r\n", &tok_status);

  *reason = apr_pstrdup(result_pool, token);

  return SVN_NO_ERROR;
}


typedef struct error_item_t
{
  const char *path;
  const char *propname;

  int http_status;
  const char *http_reason;
  apr_status_t apr_err;

  const char *message;
} error_item_t;

static svn_error_t *
multistatus_opened(svn_ra_serf__xml_estate_t *xes,
                   void *baton,
                   int entered_state,
                   const svn_ra_serf__dav_props_t *tag,
                   apr_pool_t *scratch_pool)
{
  /*struct svn_ra_serf__server_error_t *server_error = baton;*/
  const char *propname;

  switch (entered_state)
    {
      case MS_PROPSTAT_PROP_NAME:
        if (strcmp(tag->xmlns, SVN_DAV_PROP_NS_SVN) == 0)
          propname = apr_pstrcat(scratch_pool, SVN_PROP_PREFIX, tag->name,
                                 SVN_VA_NULL);
        else
          propname = tag->name;
        svn_ra_serf__xml_note(xes, MS_PROPSTAT, "propname", propname);
        break;
      case S_ERROR:
        /* This toggles an has error boolean in libsvn_ra_neon in 1.7 */
        break;
    }

  return SVN_NO_ERROR;
}

static svn_error_t *
multistatus_closed(svn_ra_serf__xml_estate_t *xes,
                   void *baton,
                   int leaving_state,
                   const svn_string_t *cdata,
                   apr_hash_t *attrs,
                   apr_pool_t *scratch_pool)
{
  struct svn_ra_serf__server_error_t *server_error = baton;
  const char *errcode;
  const char *status;

  switch (leaving_state)
    {
      case MS_RESPONSE_HREF:
        {
          apr_status_t result;
          apr_uri_t uri;

          result = apr_uri_parse(scratch_pool, cdata->data, &uri);
          if (result)
            return svn_ra_serf__wrap_err(result, NULL);
          svn_ra_serf__xml_note(xes, MS_RESPONSE, "path",
                                svn_urlpath__canonicalize(uri.path, scratch_pool));
        }
        break;
      case MS_RESPONSE_STATUS:
        svn_ra_serf__xml_note(xes, MS_RESPONSE, "status", cdata->data);
        break;
      case MS_RESPONSE_ERROR_HUMANREADABLE:
        svn_ra_serf__xml_note(xes, MS_RESPONSE, "human-readable", cdata->data);
        errcode = svn_hash_gets(attrs, "errcode");
        if (errcode)
          svn_ra_serf__xml_note(xes, MS_RESPONSE, "errcode", errcode);
        break;
      case MS_RESPONSE:
        if ((status = svn_hash__get_cstring(attrs, "status", NULL)) != NULL)
          {
            error_item_t *item;

            item = apr_pcalloc(server_error->pool, sizeof(*item));

            item->path = apr_pstrdup(server_error->pool,
                                     svn_hash_gets(attrs, "path"));

            SVN_ERR(parse_status_line(&item->http_status,
                                      &item->http_reason,
                                      status,
                                      server_error->pool,
                                      scratch_pool));

            /* Do we have a mod_dav specific message? */
            item->message = svn_hash_gets(attrs, "human-readable");

            if (item->message)
              {
                if ((errcode = svn_hash_gets(attrs, "errcode")) != NULL)
                  {
                    apr_int64_t val;

                    SVN_ERR(svn_cstring_atoi64(&val, errcode));
                    item->apr_err = (apr_status_t)val;
                  }

                item->message = apr_pstrdup(server_error->pool, item->message);
              }
            else
              item->message = apr_pstrdup(server_error->pool,
                                          svn_hash_gets(attrs, "description"));

            APR_ARRAY_PUSH(server_error->items, error_item_t *) = item;
          }
        break;


      case MS_PROPSTAT_STATUS:
        svn_ra_serf__xml_note(xes, MS_PROPSTAT, "status", cdata->data);
        break;
      case MS_PROPSTAT_ERROR_HUMANREADABLE:
        svn_ra_serf__xml_note(xes, MS_PROPSTAT, "human-readable", cdata->data);
        errcode = svn_hash_gets(attrs, "errcode");
        if (errcode)
          svn_ra_serf__xml_note(xes, MS_PROPSTAT, "errcode", errcode);
        break;
      case MS_PROPSTAT_RESPONSEDESCRIPTION:
        svn_ra_serf__xml_note(xes, MS_PROPSTAT, "description",
                              cdata->data);
        break;

      case MS_PROPSTAT:
        if ((status = svn_hash__get_cstring(attrs, "status", NULL)) != NULL)
          {
            apr_hash_t *response_attrs;
            error_item_t *item;

            response_attrs = svn_ra_serf__xml_gather_since(xes, MS_RESPONSE);
            item = apr_pcalloc(server_error->pool, sizeof(*item));

            item->path = apr_pstrdup(server_error->pool,
                                     svn_hash_gets(response_attrs, "path"));
            item->propname = apr_pstrdup(server_error->pool,
                                         svn_hash_gets(attrs, "propname"));

            SVN_ERR(parse_status_line(&item->http_status,
                                      &item->http_reason,
                                      status,
                                      server_error->pool,
                                      scratch_pool));

            /* Do we have a mod_dav specific message? */
            item->message = svn_hash_gets(attrs, "human-readable");

            if (item->message)
              {
                if ((errcode = svn_hash_gets(attrs, "errcode")) != NULL)
                  {
                    apr_int64_t val;

                    SVN_ERR(svn_cstring_atoi64(&val, errcode));
                    item->apr_err = (apr_status_t)val;
                  }

                item->message = apr_pstrdup(server_error->pool, item->message);
              }
            else
              item->message = apr_pstrdup(server_error->pool,
                                          svn_hash_gets(attrs, "description"));


            APR_ARRAY_PUSH(server_error->items, error_item_t *) = item;
          }
        break;

      case M_ERROR_HUMANREADABLE:
        svn_ra_serf__xml_note(xes, D_ERROR, "human-readable", cdata->data);
        errcode = svn_hash_gets(attrs, "errcode");
        if (errcode)
          svn_ra_serf__xml_note(xes, D_ERROR, "errcode", errcode);
        break;

      case D_ERROR:
        {
          error_item_t *item;

          item = apr_pcalloc(server_error->pool, sizeof(*item));

          item->http_status = server_error->handler->sline.code;

          /* Do we have a mod_dav specific message? */
          item->message = svn_hash__get_cstring(attrs, "human-readable",
                                                NULL);

          if (item->message)
            {
              if ((errcode = svn_hash_gets(attrs, "errcode")) != NULL)
                {
                  apr_int64_t val;

                  SVN_ERR(svn_cstring_atoi64(&val, errcode));
                  item->apr_err = (apr_status_t)val;
                }

              item->message = apr_pstrdup(server_error->pool, item->message);
            }


          APR_ARRAY_PUSH(server_error->items, error_item_t *) = item;
        }
    }
  return SVN_NO_ERROR;
}

svn_error_t *
svn_ra_serf__server_error_create(svn_ra_serf__handler_t *handler,
                                 apr_pool_t *scratch_pool)
{
  svn_ra_serf__server_error_t *server_error = handler->server_error;
  svn_error_t *err = NULL;
  int i;

  for (i = 0; i < server_error->items->nelts; i++)
    {
      const error_item_t *item;
      apr_status_t status;
      const char *message;
      svn_error_t *new_err;

      item = APR_ARRAY_IDX(server_error->items, i, error_item_t *);

      if (!item->apr_err && item->http_status == 200)
        {
          continue; /* Success code */
        }
      else if (!item->apr_err && item->http_status == 424 && item->propname)
        {
          continue; /* Failed because other PROPPATCH operations failed */
        }

      if (item->apr_err)
        status = item->apr_err;
      else
        switch (item->http_status)
          {
            case 0:
              continue; /* Not an error */
            case 301:
            case 302:
            case 303:
            case 307:
            case 308:
              status = SVN_ERR_RA_DAV_RELOCATED;
              break;
            case 403:
              status = SVN_ERR_RA_DAV_FORBIDDEN;
              break;
            case 404:
              status = SVN_ERR_FS_NOT_FOUND;
              break;
            case 409:
              status = SVN_ERR_FS_CONFLICT;
              break;
            case 412:
              status = SVN_ERR_RA_DAV_PRECONDITION_FAILED;
              break;
            case 423:
              status = SVN_ERR_FS_NO_LOCK_TOKEN;
              break;
            case 500:
              status = SVN_ERR_RA_DAV_REQUEST_FAILED;
              break;
            case 501:
              status = SVN_ERR_UNSUPPORTED_FEATURE;
              break;
            default:
              if (err)
                status = err->apr_err; /* Just use previous */
              else
                status = SVN_ERR_RA_DAV_REQUEST_FAILED;
              break;
        }

      if (item->message && *item->message)
        {
          svn_stringbuf_t *sb = svn_stringbuf_create(item->message,
                                                     scratch_pool);

          svn_stringbuf_strip_whitespace(sb);
          message = sb->data;
        }
      else if (item->propname)
        {
          message = apr_psprintf(scratch_pool,
                                 _("Property operation on '%s' failed"),
                                 item->propname);
        }
      else
        {
          /* Yuck: Older servers sometimes assume that we get convertable
                   apr error codes, while mod_dav_svn just produces a blank
                   text error, because err->message is NULL. */
          serf_status_line sline;
          svn_error_t *tmp_err;

          memset(&sline, 0, sizeof(sline));
          sline.code = item->http_status;
          sline.reason = item->http_reason;

          tmp_err = svn_ra_serf__error_on_status(sline, item->path, NULL);

          message = (tmp_err && tmp_err->message)
                       ? apr_pstrdup(scratch_pool, tmp_err->message)
                       : _("<blank error>");
          svn_error_clear(tmp_err);
        }

      SVN_ERR_ASSERT(status > 0);
      new_err = svn_error_create(status, NULL, message);

      if (item->propname)
        new_err = svn_error_createf(new_err->apr_err, new_err,
                                    _("While handling the '%s' property on '%s':"),
                                    item->propname, item->path);
      else if (item->path)
        new_err = svn_error_createf(new_err->apr_err, new_err,
                                    _("While handling the '%s' path:"),
                                    item->path);

      err = svn_error_compose_create(
                    err,
                    new_err);
    }

  /* Theoretically a 207 status can have a 'global' description without a
     global STATUS that summarizes the final result of property/href
     operations.

     We should wrap that around the existing errors if there is one.

     But currently I don't see how mod_dav ever sets that value */

  if (!err)
    {
      /* We should fail.... but why... Who installed us? */
      err = svn_error_trace(svn_ra_serf__unexpected_status(handler));
    }

  return err;
}


svn_error_t *
svn_ra_serf__setup_error_parsing(svn_ra_serf__server_error_t **server_err,
                                 svn_ra_serf__handler_t *handler,
                                 svn_boolean_t expect_207_only,
                                 apr_pool_t *result_pool,
                                 apr_pool_t *scratch_pool)
{
  svn_ra_serf__server_error_t *ms_baton;
  svn_ra_serf__handler_t *tmp_handler;

  int *expected_status = apr_pcalloc(result_pool,
                                     2 * sizeof(expected_status[0]));

  expected_status[0] = handler->sline.code;

  ms_baton = apr_pcalloc(result_pool, sizeof(*ms_baton));
  ms_baton->pool = result_pool;

  ms_baton->items = apr_array_make(result_pool, 4, sizeof(error_item_t *));
  ms_baton->handler = handler;

  ms_baton->xmlctx = svn_ra_serf__xml_context_create(multistatus_ttable,
                                                     multistatus_opened,
                                                     multistatus_closed,
                                                     NULL,
                                                     ms_baton,
                                                     ms_baton->pool);

  tmp_handler = svn_ra_serf__create_expat_handler(handler->session,
                                                  ms_baton->xmlctx,
                                                  expected_status,
                                                  result_pool);

  /* Ugly way to obtain expat_handler() */
  tmp_handler->sline = handler->sline;
  ms_baton->response_handler = tmp_handler->response_handler;
  ms_baton->response_baton = tmp_handler->response_baton;

  *server_err = ms_baton;
  return SVN_NO_ERROR;
}



/* Implements svn_ra_serf__response_handler_t */
svn_error_t *
svn_ra_serf__handle_multistatus_only(serf_request_t *request,
                                     serf_bucket_t *response,
                                     void *baton,
                                     apr_pool_t *scratch_pool)
{
  svn_ra_serf__handler_t *handler = baton;

  /* This function is just like expect_empty_body() except for the
     XML parsing callbacks. We are looking for very limited pieces of
     the multistatus response.  */

  /* We should see this just once, in order to initialize SERVER_ERROR.
     At that point, the core error processing will take over. If we choose
     not to parse an error, then we'll never return here (because we
     change the response handler).  */
  SVN_ERR_ASSERT(handler->server_error == NULL);

    {
      serf_bucket_t *hdrs;
      const char *val;

      hdrs = serf_bucket_response_get_headers(response);
      val = serf_bucket_headers_get(hdrs, "Content-Type");
      if (val && strncasecmp(val, "text/xml", sizeof("text/xml") - 1) == 0)
        {
          svn_ra_serf__server_error_t *server_err;

          SVN_ERR(svn_ra_serf__setup_error_parsing(&server_err,
                                                   handler,
                                                   TRUE,
                                                   handler->handler_pool,
                                                   handler->handler_pool));

          handler->server_error = server_err;
        }
      else
        {
          /* The body was not text/xml, so we don't know what to do with it.
             Toss anything that arrives.  */
          handler->discard_body = TRUE;
        }
    }

  /* Returning SVN_NO_ERROR will return APR_SUCCESS to serf, which tells it
     to call the response handler again. That will start up the XML parsing,
     or it will be dropped on the floor (per the decision above).  */
  return SVN_NO_ERROR;
}

svn_error_t *
svn_ra_serf__handle_server_error(svn_ra_serf__server_error_t *server_error,
                                 svn_ra_serf__handler_t *handler,
                                 serf_request_t *request,
                                 serf_bucket_t *response,
                                 apr_status_t *serf_status,
                                 apr_pool_t *scratch_pool)
{
  svn_error_t *err;

  err = server_error->response_handler(request, response,
                                       server_error->response_baton,
                                       scratch_pool);
 /* If we do not receive an error or it is a non-transient error, return
     immediately.

     APR_EOF will be returned when parsing is complete.

     APR_EAGAIN & WAIT_CONN may be intermittently returned as we proceed through
     parsing and the network has no more data right now.  If we receive that,
     clear the error and return - allowing serf to wait for more data.
     */
  if (!err || SERF_BUCKET_READ_ERROR(err->apr_err))
    return svn_error_trace(err);

  if (!APR_STATUS_IS_EOF(err->apr_err))
    {
      *serf_status = err->apr_err;
      svn_error_clear(err);
      return SVN_NO_ERROR;
    }

  /* Clear the EOF. We don't need it as subversion error.  */
  svn_error_clear(err);
  *serf_status = APR_EOF;

  /* On PROPPATCH we always get status 207, which may or may not imply an
     error status, but let's keep it generic and just do the check for
     any multistatus */
  if (handler->sline.code == 207 /* MULTISTATUS */)
    {
      svn_boolean_t have_error = FALSE;
      int i;

      for (i = 0; i < server_error->items->nelts; i++)
        {
          const error_item_t *item;
          item = APR_ARRAY_IDX(server_error->items, i, error_item_t *);

          if (!item->apr_err && item->http_status == 200)
            {
              continue; /* Success code */
            }

          have_error = TRUE;
          break;
        }

      if (! have_error)
        handler->server_error = NULL; /* We didn't have a server error */
    }

  return SVN_NO_ERROR;
}
