You are here

invoice_api.inc in Invoice 7

File

invoice_api.inc
View source
<?php

/**
 * Page callback to generate token.
 */
function _invoice_api_session_token() {
  drupal_add_http_header('Content-Type', 'text/plain');
  print drupal_get_token('_invoice_api_session_token');
  drupal_exit();
}

/**
 * Returns the response formats that the API supports
 *
 * @return array
 */
function _invoice_api_get_supported_response_formats() {
  return array(
    'json',
    'pdf',
    'html',
  );
}

/**
 * Checks if the specified response format is supported by the API
 *
 * @param string $format
 */
function _invoice_api_check_response_format_support($format) {
  if (!in_array(strtolower($format), _invoice_api_get_supported_response_formats())) {
    _invoice_api_http_response_code(406);
    echo json_encode(array(
      'code' => 406,
      'message' => 'Not Acceptable',
    ));
    exit;
  }
  else {
    switch (strtolower($format)) {
      case 'json':
        $contentType = 'application/json';
        break;
      case 'pdf':
        $contentType = 'application/pdf';
        break;
      case 'html':
      default:
        $contentType = 'text/html';
        break;
    }
    drupal_add_http_header('Content-type', $contentType);
  }
}

/**
 * Invoice API dispatcher
 */
function _invoice_api_dispatch() {
  $menuItem = menu_get_item();
  $pageCallback = isset($menuItem['page_callback']) ? $menuItem['page_callback'] : null;
  $pageArguments = isset($menuItem['page_arguments']) ? $menuItem['page_arguments'] : array();

  // Check for API route
  if (strpos($pageCallback, 'invoice_api') === 0) {
    $GLOBALS['invoice_api'] = true;
    if (isset($menuItem['page_arguments'][0])) {
      $format = $menuItem['page_arguments'][0];
      $pageArguments[0] = str_replace('invoice.', '', $format);
    }
    else {
      $format = null;
    }
    _invoice_api_check_response_format_support(str_replace('invoice.', '', $format));
    _invoice_api_authenticate();

    // Prepend request method
    array_unshift($pageArguments, strtoupper($_SERVER['REQUEST_METHOD']));
    call_user_func_array($pageCallback, $pageArguments);
    exit;
  }
}

/**
 * Authenticates through basic HTTP authentication
 */
function _invoice_api_authenticate() {

  /**
   * Remove any previous sessions to stay stateless
   */
  $sessionId = session_id();
  _drupal_session_destroy($sessionId);
  _invoice_api_check_user_pass();
  _invoice_api_check_allowed_ips();
}

/**
 * Verifies the username and password
 */
function _invoice_api_check_user_pass() {
  require_once DRUPAL_ROOT . '/' . variable_get('password_inc', 'includes/password.inc');
  $edit['name'] = isset($_SERVER['PHP_AUTH_USER']) ? $_SERVER['PHP_AUTH_USER'] : '';
  $edit['pass'] = isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : '';
  $users = user_load_multiple(array(), array(
    'name' => $edit['name'],
    'status' => 1,
  ));
  $account = reset($users);
  if (user_check_password($edit['pass'], $account)) {

    // Login the user
    $GLOBALS['user'] = $account;

    // Update the user table timestamp noting user has logged in.
    // This is also used to invalidate one-time login links.

    /*$GLOBALS['user']->login = REQUEST_TIME;
            db_update('users')
              ->fields(array('login' => $GLOBALS['user']->login))
              ->condition('uid', $GLOBALS['user']->uid)
              ->execute();

            // Regenerate the session ID to prevent against session fixation attacks.
            // This is called before hook_user in case one of those functions fails
            // or incorrectly does a redirect which would leave the old session in place.
            drupal_session_regenerate();*/
  }
  else {

    // Authentication failed
    _invoice_api_http_response_code(401);
    drupal_add_http_header('WWW-Authenticate', 'Basic Realm="Invoice API"');
    exit;
  }
}

/**
 * Check if the request is from an allowed IP addresses
 */
function _invoice_api_check_allowed_ips() {
  $allowedIps = variable_get('invoice_api_allowed_ips', '');
  if ('' != trim($allowedIps)) {

    // Trim ips
    $ips = explode(',', $allowedIps);
    foreach ($ips as $index => $ip) {
      $ips[$index] = trim($ip);
    }
    if (!in_array($_SERVER['REMOTE_ADDR'], $ips)) {

      // Authentication failed
      _invoice_api_http_response_code(403);
      echo json_encode(array(
        'code' => 403,
        'message' => 'IP address is not allowed',
      ));
      exit;
    }
  }
}

/**
 * Checks if the authenticated username has access to the defined template name
 */
function _invoice_api_check_allowed_templates() {
  $templateAllowed = false;
  $data = _invoice_api_get_request_data();
  if (!isset($data['template']) || '' == trim($data['template'])) {
    _invoice_api_http_response_code(400);
    echo json_encode(array(
      'code' => 400,
      'message' => 'Template is required',
    ));
    exit;
  }
  if (variable_get('invoice_api_root_username') == $GLOBALS['user']->name) {
    $templateAllowed = true;
  }
  if (true !== $templateAllowed) {
    $templates = _invoice_get_templates();
    foreach ($templates as $template) {
      $username = _invoice_get_variable($template, 'api_username', '');
      if ('' != trim($username) && $username == $GLOBALS['user']->name && strtolower($template) == strtolower($data['template'])) {
        $templateAllowed = true;
        break;
      }
    }
  }
  if (true !== $templateAllowed) {
    _invoice_api_http_response_code(403);
    echo json_encode(array(
      'code' => 403,
      'message' => 'Permission denied for this template',
    ));
    exit;
  }
}

/**
 * Returns the request data
 *
 * @staticvar null|array $data
 * @return    null|array
 */
function _invoice_api_get_request_data() {
  static $data = null;
  if (null === $data) {
    $data = json_decode(file_get_contents('php://input'), true);
  }
  return $data;
}

/**
 * GET / PUT / POST / DELETE an invoice
 *
 * @param string  $requestMethod
 * @param string  $format Response format
 * @param integer $invoiceId
 */
function invoice_api_invoice($requestMethod, $format, $invoiceId = null) {
  if ($requestMethod != 'GET') {

    // Check anti-CSRF token header
    if (!isset($_SERVER['HTTP_X_CSRF_TOKEN']) || !drupal_valid_token($_SERVER['HTTP_X_CSRF_TOKEN'], '_invoice_api_session_token')) {
      _invoice_api_http_response_code(406);
      echo json_encode(array(
        'code' => 406,
        'message' => 'Token validation failed',
      ));
      exit;
    }
  }
  switch ($requestMethod) {
    case 'GET':
      if ($invoiceId > 0) {
        _invoice_api_invoice_get((int) $invoiceId, $format);
      }
      else {
        _invoice_api_invoice_get_list();
      }
      break;
    case 'POST':
      _invoice_api_invoice_post($format);
      break;
    case 'PUT':
      _invoice_api_invoice_put((int) $invoiceId);
      break;
    case 'DELETE':
      _invoice_api_http_response_code(501);
      echo json_encode(array(
        'code' => 501,
        'message' => 'Invoices may never be deleted,' . ' create a credit invoice instead.',
      ));
      exit;
      break;
    default:

      // Method not allowed
      _invoice_api_http_response_code(405);
      echo json_encode(array(
        'code' => 405,
        'message' => 'Method Not Allowed',
      ));
      exit;
  }
}

/**
 * Handles GET request for a list of invoices
 */
function _invoice_api_invoice_get_list() {
  $options = array(
    'page' => isset($_GET['page']) ? $_GET['page'] : '',
    // Picked up by PagerDefault
    'max_results' => isset($_GET['max_results']) && $_GET['max_results'] > 0 ? $_GET['max_results'] : 15,
    'sort' => isset($_GET['sort']) && '' != trim($_GET['sort']) ? preg_replace('/[^a-zA-Z0-9\\.]/', '', $_GET['sort']) : 'n.nid',
    'order' => isset($_GET['order']) && in_array($_GET['order'], array(
      'asc',
      'desc',
    )) ? $_GET['order'] : 'desc',
    'template' => isset($_GET['template']) ? $_GET['template'] : '',
    'customer_number' => isset($_GET['customer_number']) ? $_GET['customer_number'] : '',
    'year' => isset($_GET['year']) ? $_GET['year'] : '',
    'month' => isset($_GET['month']) ? $_GET['month'] : '',
    'day' => isset($_GET['day']) ? $_GET['day'] : '',
  );
  $createDates = array();
  if ('' != $options['year'] || '' != $options['month'] || '' != $options['day']) {
    $year = '' != $options['year'] ? $options['year'] : date('Y');
    $month = '' != $options['month'] ? $options['month'] : date('m');
    $startDay = '' != $options['day'] ? $options['day'] : 1;
    $endDay = '' != $options['day'] ? $options['day'] : cal_days_in_month(CAL_GREGORIAN, $month, $year);
    $createStart = new DateTime();
    $createStart
      ->setDate($year, $month, $startDay)
      ->setTime(0, 0, 0);
    $createDates['start'] = $createStart;
    $createEnd = new DateTime();
    $createEnd
      ->setDate($year, $month, $endDay)
      ->setTime(23, 59, 59);
    $createDates['end'] = $createEnd;
  }
  try {
    $query = db_select('invoice_invoices', 'ii')
      ->extend('PagerDefault')
      ->limit($options['max_results']);
    $query
      ->fields('ii', array(
      'iid',
      'nid',
      'leading_zeros',
      'prefix',
      'pay_limit',
      'pay_status',
      'description',
      'uid',
    ));
    $query
      ->fields('c', array(
      'customer_number',
      'company_name',
      'lastname',
      'firstname',
    ));
    $query
      ->fields('n', array(
      'created',
      'changed',
    ));
    $query
      ->addExpression('it.name', 'template');
    $query
      ->leftJoin('node', 'n', 'ii.nid = n.nid');
    $query
      ->leftJoin('invoice_customers', 'c', 'ii.iid = c.invoice_id');
    $query
      ->leftJoin('invoice_templates', 'it', 'ii.tid = it.tid');
    $query
      ->groupBy('ii.iid')
      ->orderBy($options['sort'], strtoupper($options['order']));
    $count_query = db_select('invoice_invoices', 'ii');
    $count_query
      ->leftJoin('node', 'n', 'ii.nid = n.nid');
    $count_query
      ->leftJoin('invoice_customers', 'c', 'ii.iid = c.invoice_id');
    $count_query
      ->leftJoin('invoice_templates', 'it', 'ii.tid = it.tid');
    $count_query
      ->addExpression('COUNT(*)');
    if ('' != $options['template']) {
      $query
        ->condition('it.name', $options['template'], '=');
      $count_query
        ->condition('it.name', $options['template'], '=');
    }
    if ('' != $options['customer_number']) {
      $query
        ->condition('c.customer_number', $options['customer_number'], '=');
      $count_query
        ->condition('c.customer_number', $options['customer_number'], '=');
    }
    if (2 == count($createDates)) {
      $query
        ->condition('n.created', $createDates['start']
        ->getTimestamp(), '>=');
      $query
        ->condition('n.created', $createDates['end']
        ->getTimestamp(), '<=');
      $count_query
        ->condition('n.created', $createDates['start']
        ->getTimestamp(), '>=');
      $count_query
        ->condition('n.created', $createDates['end']
        ->getTimestamp(), '<=');
    }
    $query
      ->setCountQuery($count_query);
    $result = $query
      ->execute();
  } catch (\Exception $e) {
    _invoice_api_http_response_code(500);
    echo json_encode(array(
      'code' => 500,
      'message' => $e
        ->getMessage(),
    ));
    exit;
  }
  $rows = array();
  foreach ($result as $row) {

    // Set locale so money has the right format for the preferred culture
    $locale = _invoice_get_variable($row->template, 'locale');
    if ($locale) {
      setlocale(LC_MONETARY, $locale);
    }

    // Get invoice totals
    $totals = _invoice_get_invoice_totals($row->iid);

    // Add row
    $rows[] = array(
      'id' => (int) $row->iid,
      'number' => _invoice_get_formatted_invoice_number($row->iid, NULL, $row->created),
      'leading_zeros' => (int) $row->leading_zeros,
      'prefix' => $row->prefix,
      'template' => $row->template,
      'description' => $row->description,
      'extotal' => $totals['extotal'],
      'inctotal' => $totals['inctotal'],
      'vattotal' => $totals['vattotal'],
      'formatted_extotal' => _invoice_round_and_format_money($totals['extotal'], 2),
      'formatted_inctotal' => _invoice_round_and_format_money($totals['inctotal'], 2),
      'formatted_vattotal' => _invoice_round_and_format_money($totals['vattotal'], 2),
      'pay_status' => $row->pay_status,
      'pay_limit' => (int) $row->pay_limit,
      'uid' => (int) $row->uid,
      'created' => date('Y-m-d H:i:s', $row->created),
      'changed' => date('Y-m-d H:i:s', $row->changed),
      'customer' => array(
        'customer_number' => $row->customer_number,
        'company_name' => $row->company_name,
        'firstname' => $row->firstname,
        'lastname' => $row->lastname,
      ),
    );
  }
  _invoice_api_http_response_code(200);

  // OK
  echo json_encode($rows);
}

/**
 * Handles GET request for the specified invoice ID
 *
 * @param integer $invoiceId
 * @param string  $format Valid values: html, json or pdf
 */
function _invoice_api_invoice_get($invoiceId, $format) {
  if (0 === $invoiceId) {
    _invoice_api_http_response_code(400);
    echo json_encode(array(
      'code' => 400,
      'message' => 'Invoice ID invalid or missing',
    ));
    exit;
  }
  else {
    $row = db_query("SELECT COUNT(iid) AS count, it.api_username FROM {invoice_invoices} ii\n            JOIN {invoice_templates} it ON ii.tid = it.tid\n            WHERE iid = :invoiceId\n            LIMIT 1", array(
      ':invoiceId' => $invoiceId,
    ))
      ->fetchAssoc();
    if ($row['count'] < 1) {
      _invoice_api_http_response_code(404);
      echo json_encode(array(
        'code' => 404,
        'message' => 'Not Found',
      ));
      exit;
    }
    if ($row['api_username'] !== $GLOBALS['user']->name && variable_get('invoice_api_root_username') !== $GLOBALS['user']->name) {
      _invoice_api_http_response_code(403);
      echo json_encode(array(
        'code' => 403,
        'message' => 'No access to the template of this invoice',
      ));
      exit;
    }
  }
  _invoice_api_http_response_code(200);

  // OK
  switch ($format) {
    case 'html':
      invoice_view_print($invoiceId);
      break;
    case 'pdf':
      invoice_view_pdf($invoiceId);
      break;
    case 'json':
    default:
      _invoice_api_view_json($invoiceId);
      break;
  }
}

/**
 * Handles POST request
 *
 * @param string $format Response format
 */
function _invoice_api_invoice_post($format) {
  _invoice_api_check_allowed_templates();
  $data = _invoice_api_get_request_data();
  $invoiceModel = new InvoiceRestModel();
  $invoiceModel
    ->exchangeArray($data);
  $transaction = db_transaction();
  try {
    $node = new stdClass();
    $node->type = 'invoice';
    $node->title = null;
    $node->language = LANGUAGE_NONE;
    $node->invoice_number = null;
    $node->user_defined_invoice_number = '';
    $invoiceModel
      ->mapToNode($node);

    // Set some default values
    node_object_prepare($node);

    // Prepare node for a submit
    $node = node_submit($node);

    // Save the node. A nid property is available after this call.
    node_save($node);
  } catch (Exception $e) {
    $transaction
      ->rollback();
    watchdog_exception('invoice_api', $e);
    _invoice_api_http_response_code(500);
    exit;
  }
  _invoice_api_http_response_code(201);
  $uri = _invoice_get_transfer_protocol() . '://' . $_SERVER['HTTP_HOST'] . '/invoice/api/invoice.' . $format . '/' . $node->invoice_number;
  echo json_encode(array(
    '_links' => array(
      'self' => array(
        'href' => $uri,
      ),
    ),
  ));
  drupal_add_http_header('Location', $uri);
}

/**
 * Handles PUT request
 *
 * @param integer $invoiceId
 */
function _invoice_api_invoice_put($invoiceId) {
  if (0 === $invoiceId) {
    _invoice_api_http_response_code(400);
    echo json_encode(array(
      'code' => 400,
      'message' => 'Invoice ID invalid or missing',
    ));
    exit;
  }
  else {
    $count = db_query("SELECT COUNT(iid) FROM {invoice_invoices} WHERE iid = :invoiceId", array(
      ':invoiceId' => $invoiceId,
    ))
      ->fetchField();
    if ($count < 1) {
      _invoice_api_http_response_code(404);
      echo json_encode(array(
        'code' => 404,
        'message' => 'Not Found',
      ));
      exit;
    }
  }
  _invoice_api_check_allowed_templates();
  $data = _invoice_api_get_request_data();
  $nid = db_query("SELECT nid FROM {invoice_invoices} WHERE iid = :invoiceId", array(
    ':invoiceId' => $invoiceId,
  ))
    ->fetchField();
  $invoiceModel = new InvoiceRestModel();
  $invoiceModel
    ->exchangeArray($data);
  $transaction = db_transaction();
  try {
    $node = node_load($nid);
    $invoiceModel
      ->mapToNode($node);

    // Set some default values
    node_object_prepare($node);
    if (!empty($node->menu) && isset($node->menu['link']['enabled'])) {

      // Calling menu_node_prepare(), menu_node_save()
      // will cause data loss unless the enabled flag is set.
      $node->menu['link']['enabled'] = (int) (bool) $node->menu['link']['mlid'];
    }
    $node->invoice_number = $invoiceId;
    $node->user_defined_invoice_number = '';

    // Prepare node for a submit
    $node = node_submit($node);

    // Save the node.
    node_save($node);
  } catch (Exception $e) {
    $transaction
      ->rollback();
    watchdog_exception('invoice_api', $e);
    _invoice_api_http_response_code(500);
    exit;
  }
  _invoice_api_http_response_code(204);

  // No content
}

/**
 * Displays the specified invoice in JSON format
 *
 * @param integer $invoiceId
 */
function _invoice_api_view_json($invoiceId) {
  $nid = db_query("SELECT nid FROM {invoice_invoices} WHERE iid = :invoiceId", array(
    ':invoiceId' => $invoiceId,
  ))
    ->fetchField();
  $invoiceModel = new InvoiceRestModel();
  $node = node_load($nid);
  $invoiceModel
    ->mapFromNode($node);
  _invoice_api_http_response_code(200);

  // OK
  echo json_encode($invoiceModel
    ->getArrayCopy());
}

/**
 * Sets HTTP response code and message
 *
 * @param  integer $code
 * @return integer
 */
function _invoice_api_http_response_code($code = NULL) {
  if ($code !== NULL) {
    switch ($code) {
      case 100:
        $text = 'Continue';
        break;
      case 101:
        $text = 'Switching Protocols';
        break;
      case 200:
        $text = 'OK';
        break;
      case 201:
        $text = 'Created';
        break;
      case 202:
        $text = 'Accepted';
        break;
      case 203:
        $text = 'Non-Authoritative Information';
        break;
      case 204:
        $text = 'No Content';
        break;
      case 205:
        $text = 'Reset Content';
        break;
      case 206:
        $text = 'Partial Content';
        break;
      case 300:
        $text = 'Multiple Choices';
        break;
      case 301:
        $text = 'Moved Permanently';
        break;
      case 302:
        $text = 'Moved Temporarily';
        break;
      case 303:
        $text = 'See Other';
        break;
      case 304:
        $text = 'Not Modified';
        break;
      case 305:
        $text = 'Use Proxy';
        break;
      case 400:
        $text = 'Bad Request';
        break;
      case 401:
        $text = 'Unauthorized';
        break;
      case 402:
        $text = 'Payment Required';
        break;
      case 403:
        $text = 'Forbidden';
        break;
      case 404:
        $text = 'Not Found';
        break;
      case 405:
        $text = 'Method Not Allowed';
        break;
      case 406:
        $text = 'Not Acceptable';
        break;
      case 407:
        $text = 'Proxy Authentication Required';
        break;
      case 408:
        $text = 'Request Time-out';
        break;
      case 409:
        $text = 'Conflict';
        break;
      case 410:
        $text = 'Gone';
        break;
      case 411:
        $text = 'Length Required';
        break;
      case 412:
        $text = 'Precondition Failed';
        break;
      case 413:
        $text = 'Request Entity Too Large';
        break;
      case 414:
        $text = 'Request-URI Too Large';
        break;
      case 415:
        $text = 'Unsupported Media Type';
        break;
      case 500:
        $text = 'Internal Server Error';
        break;
      case 501:
        $text = 'Not Implemented';
        break;
      case 502:
        $text = 'Bad Gateway';
        break;
      case 503:
        $text = 'Service Unavailable';
        break;
      case 504:
        $text = 'Gateway Time-out';
        break;
      case 505:
        $text = 'HTTP Version not supported';
        break;
      default:
        exit('Unknown http status code "' . htmlentities($code) . '"');
        break;
    }
    $protocol = isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0';
    header($protocol . ' ' . $code . ' ' . $text);
    $GLOBALS['http_response_code'] = $code;
  }
  else {
    $code = isset($GLOBALS['http_response_code']) ? $GLOBALS['http_response_code'] : 200;
  }
  return $code;
}

Functions

Namesort descending Description
invoice_api_invoice GET / PUT / POST / DELETE an invoice
_invoice_api_authenticate Authenticates through basic HTTP authentication
_invoice_api_check_allowed_ips Check if the request is from an allowed IP addresses
_invoice_api_check_allowed_templates Checks if the authenticated username has access to the defined template name
_invoice_api_check_response_format_support Checks if the specified response format is supported by the API
_invoice_api_check_user_pass Verifies the username and password
_invoice_api_dispatch Invoice API dispatcher
_invoice_api_get_request_data Returns the request data
_invoice_api_get_supported_response_formats Returns the response formats that the API supports
_invoice_api_http_response_code Sets HTTP response code and message
_invoice_api_invoice_get Handles GET request for the specified invoice ID
_invoice_api_invoice_get_list Handles GET request for a list of invoices
_invoice_api_invoice_post Handles POST request
_invoice_api_invoice_put Handles PUT request
_invoice_api_session_token Page callback to generate token.
_invoice_api_view_json Displays the specified invoice in JSON format