<?php
// vim: ff=unix fenc=utf8 tw=80 ts=2 sw=2 et ai
// $Id$

/**
 * Implementation of hook_server_info().
 */
function rest_server_server_info() {
  return array(
    '#name' => 'REST',
    '#path' => 'rest',
  );
}


/**
 * Implementation of hook_server()
 */
function rest_server_server() {
    
  $req = _rest_server_get_request();
  $method_name = $req['service'] . '.' . $req['method'];
  $found = false;
  $args = array();
  
  // Check for which service we're calling
  foreach (services_get_all() as $method) {
    if ($method['#method'] === $method_name) {
      // Flag the method as found
      $found = true;
      
      // Make sure that include files are indeed included
      if (isset($method['#file']) && is_array($include = $method['#file'])) {
        module_load_include($include['file'], $include['module']);
      }
      
      // Get a list of service specific params to assign values by path order
      // The services module inject authentication related arguments as first on
      // the list so we need to map ordered arguments from the path to the
      // argument names in the selected service
      $values = array();
      if (!empty($req['args'])) {
        $methods = module_invoke($method['#file']['module'], 'service');
        if ($methods) {
          foreach ($methods as $m) {
            if ($m['#method'] === $method_name) {
              foreach ($m['#args'] as $idx=>$arg ) {
                if (!isset($req['args'][$idx])) {
                  break;
                }
                $values[ $arg['#name'] ] = $req['args'][$idx];
              }
            }
          }
        }
      }
      
      // Special use cases for arguments of certain types
      foreach ($method['#args'] as $argument) {
        $argname = $argument['#name'];
        
        // Get the argument value (request params have priority over path ones)
        if (isset($_REQUEST[$argname])) {
          $argvalue = $_REQUEST[$argname];
        } else if (isset($values[$argname])) {
          $argvalue = $values[$argname];
        } else {
          $argvalue = '';
        }
        
        switch ($argument['#type']) {
          case 'array' :
            $args[] = empty($argvalue) ? array() : explode(',', $argvalue);
            break;
          case 'string':
            $args[] = empty($argvalue) ? '' : $argvalue;
            break;
          default:
            $args[] = $argvalue;
            break;
        }
      }
      break;
    }
  }

  try {
    $response = services_method_call($method_name, $args);

    // If 'status' is set, an error ocurred.
    if (is_array($response) && isset($response['status'])) {
      $result = $response;
    }
    else {
      $result = array(
        'status' => 'success',
        'response' => $response,
      );
    }
  }
  catch (Exception $e) {
    $result = rest_server_server_error($e->getMessage());    
  }

  if ($result['status'] === 'error') {
    // log the error in the watchdog
    watchdog('rest_server', $result['response'], NULL, WATCHDOG_ERROR);
    
    //!TODO: Can we further inspect the error type from the message?
    if ($found) {
        drupal_set_header('HTTP/1.1 403 Forbidden');
    } else {
        drupal_set_header('HTTP/1.1 404 Not Found');
    }
  }


  switch ($req['format']) {
    case 'json':
      drupal_set_header('Content-Type: application/json; charset=utf-8');
      echo _rest_server_serialize_json($result);
      break;
    case 'php':
      drupal_set_header('Content-Type: application/php; charset=utf-8');
      echo _rest_server_serialize_php($result);
      break;
    case 'xmlrpc':      
      drupal_set_header('Content-Type: text/xml; charset=utf-8');
      echo _rest_server_serialize_xmlrpc($result);
      break;
    case 'xml':
    default:
      drupal_set_header('Content-Type: text/xml; charset=utf-8');
      echo _rest_server_serialize_xml($result);
  }  
}

/**
 * Implementation of hook_server_error
 *
 * @param string $message
 * @return array
 */
function rest_server_server_error($message) {  
  return array(
    'status' => 'error',
    'message' => t($message),
  );
}

/**
 * Convert data array to xml representation.
 *
 * @param $data
 *   Array or object with data to convert.
 * @param $xml
 *   SimpleXML object, used in recursive calls.
 * @param $depth
 *   Node depth, used in recursive calls.
 *
 * @return
 *   XML string.
 */
function _rest_server_serialize_xml($data, $xml = NULL, $depth = 0)  {
  if (!isset($xml)) {
    $xml = simplexml_load_string('<?xml version="1.0" encoding="utf-8"?><rest generator="xml_writer" version="1.0" />');
  }

  foreach ($data as $key => $value) {
    
    if (is_array($value)|| is_object($value)) {
      if (is_numeric($key)) {
        $child = $xml->addChild('item');
        $child->addAttribute('id', $key);
      } else {
        $child = $xml->addChild($key);
      }
      
      _rest_server_serialize_xml($value, $child, $depth + 1);
    }
    else {
      $value = htmlspecialchars($value);
      if (is_numeric($key)) {
        $xml->addChild('item', $value)
            ->addAttribute('id', $key);
      }
      else {
        $xml->addChild($key, $value);
      }
    }
  }

  if ($depth == 0) {
    return $xml->asXML();
  }
}

/**
 * Helper function to serialize to a JSON string
 *
 * @param $data Array or Object
 * @return string
 */
function _rest_server_serialize_json($data) {
  if (function_exists('json_encode')) {
    return json_encode($data);
  } else {
    return drupal_to_js($data);
  }
}

/**
 * Helper function to serialize to a PHP serialize string
 *
 * @param $data Array or Object
 * @return string
 */
function _rest_server_serialize_php($data) {
  return serialize($data);
}


/**
 * Helper function to serialize in XMLRPC format
 *
 * @param $data Array or Object
 * @return string
 */
function _rest_server_serialize_xmlrpc($data) {
  
  include_once './includes/xmlrpc.inc';
  
  if ($data['status'] === 'error') {
    
    // Encode the error
    $r = xmlrpc_error(0, $data['message']);
    $ret = xmlrpc_error_get_xml($r);
    
  } else {
    
    // Encode the result
    $r = xmlrpc_value($data['response']);
    $ret = '
      <methodResponse>
        <params>
        <param>
          <value>' .
          xmlrpc_value_get_xml($r) .
          '</value>
        </param>
        </params>
      </methodResponse>';
  }
  
  return '<?xml version="1.0" encoding="utf-8"?>' . $ret;
}

/**
 * Helper function to analyze the request
 *
 * @return Array
 */
function _rest_server_get_request() {
  $req = array(
    'format' => 'xml',
    'service' => '',
    'method' => '',
    'args' => array()
  );
  
  // Get the URI path
  $path = parse_url(request_uri(), PHP_URL_PATH);
  
  // Check the format in the query argument
  if (!empty($_GET['format'])) {
      
    $req['format'] = strtolower($_GET['format']);
  
  // Check if a 'file extension' like format is used
  } else if (preg_match('#\.([A-Za-z]+)$#', $path, $m)) {
      
    $req['format'] = strtolower($m[1]);
    // Remove the format from the path if pressent    
    $path = substr($path, 0, strrpos($path, '.'));        
      
  // Check Accept header
  } else if (!empty($_SERVER['HTTP_ACCEPT'])) {
      
    $mimes = explode(',', $_SERVER['HTTP_ACCEPT']);
    foreach ( $mimes as $mime ) {
      $mime = strtolower($mime);
      
      if ( FALSE !== strpos($mime, '/xml+rpc') ) {
        $req['format'] = 'xmlrpc';
        break;        
      } else if ( FALSE !== strpos($mime, '/xml') ) {
        $req['format'] = 'xml';
        break;
      } else if ( FALSE !== strpos($mime, '/json') ) {
        $req['format'] = 'json';
        break;
      } else if ( FALSE !== strpos($mime, '/javascript') ) {
        $req['format'] = 'json';
        break;
      } else if ( FALSE !== strpos($mime, '/php') ) {
        $req['format'] = 'php';
        break;
      }
    }
  }
  
  
  $info = rest_server_server_info();
  $prefix = 'services/' . $info['#path'] . '/';
  
  // Get the path below the service server prefix
  $path = substr($path, strpos($path, $prefix) + strlen($prefix));
  $parts = explode('/', $path);    
  foreach ($parts as $idx=>$part) {
    $part = rawurldecode($part);
    switch ($idx) {
      case 0:
        $req['service'] = $part;
      break;
      case 1:
        $req['method'] = $part;
      break;
      default:
        $req['args'][] = $part;
    }
  }
  
  // Override the method if pressent as a query/body argument
  if (!empty($_REQUEST['method'])) {
    list($req['service'], $req['method']) = explode('.', $_REQUEST['method']);
  }
  
  // If no session id is supplied and we don't have it required then use the
  // automatically generated one (anonymous unless there is a valid cookie)
  if (!isset($_REQUEST['sessid'])) {
    $_REQUEST['sessid'] = session_id();
  }
  
  return $req;
}