Merge pull request #2006 from civicrm/4.3
[civicrm-core.git] / api / v3 / utils.php
index e8592987c838db5aee4c549b6a7583c1107b9138..36e84b908049ef1351e0822e90be48a1188d283b 100644 (file)
@@ -1,9 +1,7 @@
 <?php
-// $Id$
-
 /*
   +--------------------------------------------------------------------+
-  | CiviCRM version 4.3                                                |
+  | CiviCRM version 4.4                                                |
   +--------------------------------------------------------------------+
   | Copyright CiviCRM LLC (c) 2004-2013                                |
   +--------------------------------------------------------------------+
  * Initialize CiviCRM - should be run at the start of each API function
  */
 function _civicrm_api3_initialize() {
-  require_once 'CRM/Core/Config.php';
-  $config = CRM_Core_Config::singleton();
-  }
+  require_once 'CRM/Core/ClassLoader.php';
+  CRM_Core_ClassLoader::singleton()->register();
+  CRM_Core_Config::singleton();
+}
 
 /**
  * Wrapper Function for civicrm_verify_mandatory to make it simple to pass either / or fields for checking
  *
  * @param array $params array of fields to check
  * @param array $daoName string DAO to check for required fields (create functions only)
- * @param array $keys list of required fields options. One of the options is required
+ * @param array $keyoptions
+ *
+ * @internal param array $keys list of required fields options. One of the options is required
  * @return null or throws error if there the required fields not present
-
  * @
- *
  */
 function civicrm_api3_verify_one_mandatory($params, $daoName = NULL, $keyoptions = array(
   )) {
@@ -74,6 +73,7 @@ function civicrm_api3_verify_one_mandatory($params, $daoName = NULL, $keyoptions
  * @param array $keys list of required fields. A value can be an array denoting that either this or that is required.
  * @param bool $verifyDAO
  *
+ * @throws API_Exception
  * @return null or throws error if there the required fields not present
  *
  * @todo see notes on _civicrm_api3_check_required_fields regarding removing $daoName param
@@ -128,11 +128,12 @@ function civicrm_api3_verify_mandatory($params, $daoName = NULL, $keys = array(
 
 /**
  *
- * @param <type> $msg
  * @param <type> $data
+ * @param array $data
  * @param object $dao DAO / BAO object to be freed here
  *
- * @return <type>
+ * @throws API_Exception
+ * @return array <type>
  */
 function civicrm_api3_create_error($msg, $data = array(), &$dao = NULL) {
   //fix me - $dao should be param 4 & 3 should be $apiRequest
@@ -143,7 +144,8 @@ function civicrm_api3_create_error($msg, $data = array(), &$dao = NULL) {
   if (is_array($dao)) {
     if ($msg == 'DB Error: constraint violation' || substr($msg, 0,9)  == 'DB Error:' || $msg == 'DB Error: already exists') {
       try {
-        _civicrm_api3_validate_fields($dao['entity'], $dao['action'], $dao['params'], TRUE);
+        $fields = _civicrm_api3_api_getfields($dao);
+        _civicrm_api3_validate_fields($dao['entity'], $dao['action'], $dao['params'], $fields, TRUE);
       }
       catch(Exception $e) {
         $msg = $e->getMessage();
@@ -152,6 +154,11 @@ function civicrm_api3_create_error($msg, $data = array(), &$dao = NULL) {
   }
   $data['is_error'] = 1;
   $data['error_message'] = $msg;
+  // we will show sql to privelledged user only (not sure of a specific
+  // security hole here but seems sensible - perhaps should apply to the trace as well?
+  if(isset($data['sql']) && CRM_Core_Permission::check('Administer CiviCRM')) {
+    $data['debug_information'] = $data['sql'];
+  }
   if (is_array($dao) && isset($dao['params']) && is_array($dao['params']) && CRM_Utils_Array::value('api.has_parent', $dao['params'])) {
     $errorCode = empty($data['error_code']) ? 'chained_api_failed' : $data['error_code'];
     throw new API_Exception('Error in call to ' . $dao['entity'] . '_' . $dao['action'] . ' : ' . $msg, $errorCode, $data);
@@ -183,20 +190,17 @@ function civicrm_api3_create_success($values = 1, $params = array(
         $values[$key]['id'] = $item[$entity . "_id"];
       }
       if(!empty($item['financial_type_id'])){
+        //4.3 legacy handling
         $values[$key]['contribution_type_id'] = $item['financial_type_id'];
       }
-    }
-  }
-  //if ( array_key_exists ('debug',$params) && is_object ($dao)) {
-  if (is_array($params) && array_key_exists('debug', $params)) {
-    if (!is_object($dao)) {
-      $d = _civicrm_api3_get_DAO(CRM_Utils_Array::value('entity', $params));
-      if (!empty($d)) {
-        $file = str_replace('_', '/', $d) . ".php";
-        require_once ($file);
-        $dao = new $d();
+      if(!empty($item['next_sched_contribution_date'])){
+        // 4.4 legacy handling
+        $values[$key]['next_sched_contribution'] = $item['next_sched_contribution_date'];
       }
     }
+  }
+
+  if (is_array($params) && !empty($params['debug'])) {
     if (is_string($action) && $action != 'getfields') {
       $apiFields = civicrm_api($entity, 'getfields', array('version' => 3, 'action' => $action) + $params);
     }
@@ -212,7 +216,7 @@ function civicrm_api3_create_success($values = 1, $params = array(
       $allFields = array_keys($apiFields['values']);
     }
     $paramFields = array_keys($params);
-    $undefined = array_diff($paramFields, $allFields, array_keys($_COOKIE), array('action', 'entity', 'debug', 'version', 'check_permissions', 'IDS_request_uri', 'IDS_user_agent', 'return', 'sequential', 'rowCount', 'option_offset', 'option_limit', 'custom', 'option_sort'));
+    $undefined = array_diff($paramFields, $allFields, array_keys($_COOKIE), array('action', 'entity', 'debug', 'version', 'check_permissions', 'IDS_request_uri', 'IDS_user_agent', 'return', 'sequential', 'rowCount', 'option_offset', 'option_limit', 'custom', 'option_sort', 'options'));
     if ($undefined) {
       $result['undefined_fields'] = array_merge($undefined);
     }
@@ -223,7 +227,7 @@ function civicrm_api3_create_success($values = 1, $params = array(
 
   $result['version'] = 3;
   if (is_array($values)) {
-    $result['count'] = count($values);
+    $result['count'] = (int) count($values);
 
     // Convert value-separated strings to array
     _civicrm_api3_separate_values($values);
@@ -259,24 +263,18 @@ function _civicrm_api3_load_DAO($entity) {
   if (empty($dao)) {
     return FALSE;
   }
-  $file = str_replace('_', '/', $dao) . ".php";
-  require_once ($file);
   $d = new $dao();
   return $d;
 }
 
 /**
  * Function to return the DAO of the function or Entity
- * @param  $name is either a function of the api (civicrm_{entity}_create or the entity name
+ * @param String $name  either a function of the api (civicrm_{entity}_create or the entity name
  * return the DAO name to manipulate this function
  * eg. "civicrm_api3_contact_create" or "Contact" will return "CRM_Contact_BAO_Contact"
+ * @return mixed|string
  */
 function _civicrm_api3_get_DAO($name) {
-  static $dao = NULL;
-  if (!$dao) {
-    require ('CRM/Core/DAO/listAll.php');
-  }
-
   if (strpos($name, 'civicrm_api3') !== FALSE) {
     $last = strrpos($name, '_');
     // len ('civicrm_api3_') == 13
@@ -287,36 +285,25 @@ function _civicrm_api3_get_DAO($name) {
     $name = 'Contact';
   }
 
-  //hack to deal with incorrectly named BAO/DAO - see CRM-10859 - remove after rename
-  if($name == 'price_set'){
-    return 'CRM_Price_DAO_Set';
-  }
-  if($name == 'price_field'){
-    return 'CRM_Price_DAO_Field';
-  }
-  if($name == 'price_field_value'){
-    return 'CRM_Price_DAO_FieldValue';
-  }
-  // these aren't listed on ticket CRM-10859 - but same problem - lack of standardisation
-  if($name == 'mailing_job' || $name == 'MailingJob'){
-    return 'CRM_Mailing_BAO_Job';
-  }
+  //hack to deal with incorrectly named BAO/DAO - see CRM-10859 -
+  // several of these have been removed but am not confident mailing_recipients is
+  // tests so have not tackled.
+  // correct approach for im is unclear
   if($name == 'mailing_recipients' || $name == 'MailingRecipients'){
     return 'CRM_Mailing_BAO_Recipients';
   }
   if(strtolower($name) == 'im'){
     return 'CRM_Core_BAO_IM';
   }
-
-
-  return CRM_Utils_Array::value(_civicrm_api_get_camel_name($name, 3), $dao);
+  return CRM_Core_DAO_AllCoreTables::getFullName(_civicrm_api_get_camel_name($name, 3));
 }
 
 /**
  * Function to return the DAO of the function or Entity
- * @param  $name is either a function of the api (civicrm_{entity}_create or the entity name
+ * @param String $name is either a function of the api (civicrm_{entity}_create or the entity name
  * return the DAO name to manipulate this function
  * eg. "civicrm_contact_create" or "Contact" will return "CRM_Contact_BAO_Contact"
+ * @return mixed
  */
 function _civicrm_api3_get_BAO($name) {
   $dao = _civicrm_api3_get_DAO($name);
@@ -346,7 +333,7 @@ function _civicrm_api3_separate_values(&$values) {
 }
 
 /**
- * This is a wrapper for api_store_values which will check the suitable fields using getfields
+ * This is a legacy wrapper for api_store_values which will check the suitable fields using getfields
  * rather than DAO->fields
  *
  * Getfields has handling for how to deal with uniquenames which dao->fields doesn't
@@ -381,6 +368,7 @@ function _civicrm_api3_store_values(&$fields, &$params, &$values) {
   }
   return $valueFound;
 }
+
 /**
  * The API supports 2 types of get requestion. The more complex uses the BAO query object.
  *  This is a generic function for those functions that call it
@@ -389,9 +377,13 @@ function _civicrm_api3_store_values(&$fields, &$params, &$values) {
  *  others that use the query object. Note that this function passes permission information in.
  *  The others don't
  *
+ * @param $entity
  * @param array $params as passed into api get or getcount function
- * @param array $options array of options (so we can modify the filter)
+ * @param array $additional_options
  * @param bool $getCount are we just after the count
+ *
+ * @return
+ * @internal param array $options array of options (so we can modify the filter)
  */
 function _civicrm_api3_get_using_query_object($entity, $params, $additional_options = array(), $getCount = NULL){
 
@@ -443,7 +435,20 @@ function _civicrm_api3_get_using_query_object($entity, $params, $additional_opti
   }
 
   $newParams = CRM_Contact_BAO_Query::convertFormValues($inputParams);
+  foreach ($newParams as &$newParam) {
+    if($newParam[1] == '='  && is_array($newParam[2])) {
+      // we may be looking at an attempt to use the 'IN' style syntax
+      // @todo at time of writing only 'IN' & 'NOT IN' are  supported for the array style syntax
+      $sqlFilter = CRM_Core_DAO::createSqlFilter($newParam[0], $params[$newParam[0]], 'String', NULL, TRUE);
+      if($sqlFilter) {
+        $newParam[1] = key($newParam[2]);
+        $newParam[2] = $sqlFilter;
+      }
+    }
+
+  }
   $skipPermissions = CRM_Utils_Array::value('check_permissions', $params)? 0 :1;
+
   list($entities, $options) = CRM_Contact_BAO_Query::apiQuery(
     $newParams,
     $returnProperties,
@@ -475,6 +480,8 @@ function _civicrm_api3_dao_set_filter(&$dao, $params, $unique = TRUE, $entity) {
     //if entity_id is set then treat it as ID (will be overridden by id if set)
     $dao->id = $params[$entity . "_id"];
   }
+
+  $options = _civicrm_api3_get_options_from_params($params);
   //apply options like sort
   _civicrm_api3_apply_options_to_dao($params, $dao, $entity);
 
@@ -505,75 +512,45 @@ function _civicrm_api3_dao_set_filter(&$dao, $params, $unique = TRUE, $entity) {
     if (is_array($params[$field])) {
       //get the actual fieldname from db
       $fieldName = $allfields[$field]['name'];
-      //array is the syntax for SQL clause
-      foreach ($params[$field] as $operator => $criteria) {
-        if (in_array($operator, $acceptedSQLOperators)) {
-          switch ($operator) {
-            // unary operators
-
-            case 'IS NULL':
-            case 'IS NOT NULL':
-              $dao->whereAdd(sprintf('%s %s', $fieldName, $operator));
-              break;
-
-            // ternary operators
-
-            case 'BETWEEN':
-            case 'NOT BETWEEN':
-              if (empty($criteria[0]) || empty($criteria[1])) {
-                throw new exception("invalid criteria for $operator");
-              }
-              $dao->whereAdd(sprintf('%s ' . $operator . ' "%s" AND "%s"', $fieldName, CRM_Core_DAO::escapeString($criteria[0]), CRM_Core_DAO::escapeString($criteria[1])));
-              break;
-
-            // n-ary operators
-
-            case 'IN':
-            case 'NOT IN':
-              if (empty($criteria)) {
-                throw new exception("invalid criteria for $operator");
-              }
-              $escapedCriteria = array_map(array('CRM_Core_DAO', 'escapeString'), $criteria);
-              $dao->whereAdd(sprintf('%s %s ("%s")', $fieldName, $operator, implode('", "', $escapedCriteria)));
-              break;
-
-            // binary operators
-
-            default:
-
-              $dao->whereAdd(sprintf('%s %s "%s"', $fieldName, $operator, CRM_Core_DAO::escapeString($criteria)));
-          }
-        }
+      $where = CRM_Core_DAO::createSqlFilter($fieldName, $params[$field], 'String');
+      if(!empty($where)) {
+        $dao->whereAdd($where);
       }
     }
     else {
       if ($unique) {
-        $dao->$allfields[$field]['name'] = $params[$field];
+        $daoFieldName = $allfields[$field]['name'];
+        if (empty($daoFieldName)) {
+          throw new API_Exception("Failed to determine field name for \"$field\"");
+        }
+        $dao->{$daoFieldName} = $params[$field];
       }
       else {
         $dao->$field = $params[$field];
       }
     }
   }
-  if (!empty($params['return']) && is_array($params['return'])) {
+  if (!empty($options['return']) && is_array($options['return']) && empty($options['is_count'])) {
     $dao->selectAdd();
+    $options['return']['id'] = TRUE;// ensure 'id' is included
     $allfields =  _civicrm_api3_get_unique_name_array($dao);
-    $returnMatched = array_intersect($params['return'], $allfields);
-    $returnUniqueMatched = array_intersect(
-      array_diff(// not already matched on the field names
-        $params['return'],
-        $returnMatched),
-        array_flip($allfields)// but a match for the field keys
-    );
-
+    $returnMatched = array_intersect(array_keys($options['return']), $allfields);
     foreach ($returnMatched as $returnValue) {
-        $dao->selectAdd($returnValue);
+      $dao->selectAdd($returnValue);
     }
+
+    $unmatchedFields = array_diff(// not already matched on the field names
+      array_keys($options['return']),
+      $returnMatched
+    );
+
+    $returnUniqueMatched = array_intersect(
+      $unmatchedFields,
+      array_flip($allfields)// but a match for the field keys
+    );
     foreach ($returnUniqueMatched as $uniqueVal){
       $dao->selectAdd($allfields[$uniqueVal]);
-
     }
-    $dao->selectAdd('id');
   }
 }
 
@@ -605,12 +582,18 @@ function _civicrm_api3_apply_filters_to_dao($filterField, $filterValue, &$dao) {
 /**
  * Get sort, limit etc options from the params - supporting old & new formats.
  * get returnproperties for legacy
+ *
  * @param array $params params array as passed into civicrm_api
  * @param bool $queryObject - is this supporting a queryobject api (e.g contact) - if so we support more options
  * for legacy report & return a unique fields array
+ *
+ * @param string $entity
+ * @param string $action
+ *
  * @return array $options options extracted from params
  */
 function _civicrm_api3_get_options_from_params(&$params, $queryObject = FALSE, $entity = '', $action = '') {
+  $is_count = FALSE;
   $sort = CRM_Utils_Array::value('sort', $params, 0);
   $sort = CRM_Utils_Array::value('option.sort', $params, $sort);
   $sort = CRM_Utils_Array::value('option_sort', $params, $sort);
@@ -625,6 +608,8 @@ function _civicrm_api3_get_options_from_params(&$params, $queryObject = FALSE, $
   $limit = CRM_Utils_Array::value('option_limit', $params, $limit);
 
   if (is_array(CRM_Utils_Array::value('options', $params))) {
+    // is count is set by generic getcount not user
+    $is_count = CRM_Utils_Array::value('is_count', $params['options']);
     $offset = CRM_Utils_Array::value('offset', $params['options'], $offset);
     $limit  = CRM_Utils_Array::value('limit', $params['options'], $limit);
     $sort   = CRM_Utils_Array::value('sort', $params['options'], $sort);
@@ -641,8 +626,8 @@ function _civicrm_api3_get_options_from_params(&$params, $queryObject = FALSE, $
       $returnProperties = array_fill_keys($returnProperties, 1);
     }
   }
-  if($entity && $action =='get' ){
-    if(CRM_Utils_Array::value('id',$returnProperties)){
+  if ($entity && $action =='get') {
+    if (CRM_Utils_Array::value('id',$returnProperties)) {
       $returnProperties[$entity . '_id'] = 1;
       unset($returnProperties['id']);
     }
@@ -654,13 +639,18 @@ function _civicrm_api3_get_options_from_params(&$params, $queryObject = FALSE, $
     }
   }
 
-
   $options = array(
-    'offset' => CRM_Utils_Rule::integer($offset),
-    'sort' => CRM_Utils_Rule::string($sort),
-    'limit' => CRM_Utils_Rule::integer($limit),
+    'offset' => CRM_Utils_Rule::integer($offset) ? $offset : NULL,
+    'sort' => CRM_Utils_Rule::string($sort) ? $sort : NULL,
+    'limit' => CRM_Utils_Rule::integer($limit) ? $limit : NULL,
+    'is_count' => $is_count,
     'return' => !empty($returnProperties) ? $returnProperties : NULL,
   );
+
+  if ($options['sort'] && stristr($options['sort'], 'SELECT')) {
+    throw new API_Exception('invalid string in sort options');
+  }
+
   if (!$queryObject) {
     return $options;
   }
@@ -675,12 +665,15 @@ function _civicrm_api3_get_options_from_params(&$params, $queryObject = FALSE, $
     if (substr($n, 0, 7) == 'return.') {
       $legacyreturnProperties[substr($n, 7)] = $v;
     }
-    elseif($n == 'id'){
+    elseif ($n == 'id') {
       $inputParams[$entity. '_id'] = $v;
     }
     elseif (in_array($n, $otherVars)) {}
-    else{
-      $inputParams[$n] = CRM_Utils_Rule::string($v);
+    else {
+      $inputParams[$n] = $v;
+      if ($v && !is_array($v) && stristr($v, 'SELECT')) {
+        throw new API_Exception('invalid string');
+      }
     }
   }
   $options['return'] = array_merge($returnProperties, $legacyreturnProperties);
@@ -690,15 +683,19 @@ function _civicrm_api3_get_options_from_params(&$params, $queryObject = FALSE, $
 
 /**
  * Apply options (e.g. sort, limit, order by) to DAO object (prior to find)
+ *
  * @param array $params params array as passed into civicrm_api
  * @param object $dao DAO object
+ * @param $entity
  */
 function _civicrm_api3_apply_options_to_dao(&$params, &$dao, $entity) {
 
   $options = _civicrm_api3_get_options_from_params($params,FALSE,$entity);
-  $dao->limit((int)$options['offset'], (int)$options['limit']);
-  if (!empty($options['sort'])) {
-    $dao->orderBy($options['sort']);
+  if(!$options['is_count']) {
+    $dao->limit((int)$options['offset'], (int)$options['limit']);
+    if (!empty($options['sort'])) {
+      $dao->orderBy($options['sort']);
+    }
   }
 }
 
@@ -738,17 +735,30 @@ function _civicrm_api3_get_unique_name_array(&$bao) {
 /**
  * Converts an DAO object to an array
  *
- * @param  object   $dao           (reference )object to convert
+ * @param  object $dao           (reference )object to convert
+ * @param null $params
+ * @param bool $uniqueFields
+ * @param string $entity
+ *
+ * @return array
+ *
  * @params array of arrays (key = id) of array of fields
+ *
  * @static void
  * @access public
  */
 function _civicrm_api3_dao_to_array($dao, $params = NULL, $uniqueFields = TRUE, $entity = "") {
   $result = array();
+  if(isset($params['options']) && CRM_Utils_Array::value('is_count', $params['options'])) {
+    return $dao->count();
+  }
   if (empty($dao) || !$dao->find()) {
     return array();
   }
 
+  if(isset($dao->count)) {
+    return $dao->count;
+  }
   //if custom fields are required we will endeavour to set them . NB passing $entity in might be a bit clunky / unrequired
   if (!empty($entity) && CRM_Utils_Array::value('return', $params) && is_array($params['return'])) {
     foreach ($params['return'] as $return) {
@@ -784,9 +794,9 @@ function _civicrm_api3_dao_to_array($dao, $params = NULL, $uniqueFields = TRUE,
 /**
  * Converts an object to an array
  *
- * @param  object   $dao           (reference) object to convert
- * @param  array    $values        (reference) array
- * @param  array    $uniqueFields
+ * @param  object $dao           (reference) object to convert
+ * @param  array $values        (reference) array
+ * @param array|bool $uniqueFields
  *
  * @return array
  * @static void
@@ -820,7 +830,7 @@ function _civicrm_api3_custom_format_params($params, &$values, $extends, $entity
   $values['custom'] = array();
   foreach ($params as $key => $value) {
     list($customFieldID, $customValueID) = CRM_Core_BAO_CustomField::getKeyID($key, TRUE);
-    if ($customFieldID) {
+    if ($customFieldID && (!IS_NULL($value))) {
       CRM_Core_BAO_CustomField::formatCustomField($customFieldID, $values['custom'],
         $value, $extends, $customValueID, $entityId, FALSE, FALSE
       );
@@ -838,8 +848,11 @@ function _civicrm_api3_custom_format_params($params, &$values, $extends, $entity
  * api level. Hence the intention is to remove this function
  * & the associated param from viery_mandatory
  *
- * @param array  $params       Associative array of property name/value
+ * @param array $params       Associative array of property name/value
  *                             pairs to insert in new history.
+ * @param $daoName
+ * @param bool $return
+ *
  * @daoName string DAO to check params agains
  *
  * @return bool should the missing fields be returned as an array (core error created as default)
@@ -899,6 +912,7 @@ function _civicrm_api3_check_required_fields($params, $daoName, $return = FALSE)
  * @param $params array  params of the API call
  * @param $throw bool    whether to throw exception instead of returning false
  *
+ * @throws Exception
  * @return bool whether the current API user has the permission to make the call
  */
 function _civicrm_api3_api_check_permission($entity, $action, &$params, $throw = TRUE) {
@@ -907,8 +921,6 @@ function _civicrm_api3_api_check_permission($entity, $action, &$params, $throw =
     return TRUE;
   }
 
-  require_once 'CRM/Core/Permission.php';
-
   require_once 'CRM/Core/DAO/permissions.php';
   $permissions = _civicrm_api3_permissions($entity, $action, $params);
 
@@ -936,12 +948,15 @@ function _civicrm_api3_api_check_permission($entity, $action, &$params, $throw =
  * @param string $bao_name name of BAO
  * @param array $params params from api
  * @param bool $returnAsSuccess return in api success format
+ * @param string $entity
+ *
+ * @return array
  */
 function _civicrm_api3_basic_get($bao_name, &$params, $returnAsSuccess = TRUE, $entity = "") {
   $bao = new $bao_name();
   _civicrm_api3_dao_set_filter($bao, $params, TRUE,$entity);
   if ($returnAsSuccess) {
-    return civicrm_api3_create_success(_civicrm_api3_dao_to_array($bao, $params, FALSE, $entity), $params, $entity);
+      return civicrm_api3_create_success(_civicrm_api3_dao_to_array($bao, $params, FALSE, $entity), $params, $entity);
   }
   else {
     return _civicrm_api3_dao_to_array($bao, $params, FALSE, $entity);
@@ -953,34 +968,71 @@ function _civicrm_api3_basic_get($bao_name, &$params, $returnAsSuccess = TRUE, $
  * @param string $bao_name Name of BAO Class
  * @param array $params parameters passed into the api call
  * @param string $entity Entity - pass in if entity is non-standard & required $ids array
+ * @return array
  */
 function _civicrm_api3_basic_create($bao_name, &$params, $entity = NULL) {
 
   $args = array(&$params);
-  if(!empty($entity)){
+  if (!empty($entity)) {
     $ids = array($entity => CRM_Utils_Array::value('id', $params));
     $args[] = &$ids;
   }
+
   if (method_exists($bao_name, 'create')) {
     $fct = 'create';
+    $fct_name = $bao_name . '::' . $fct;
+    $bao = call_user_func_array(array($bao_name, $fct), $args);
   }
   elseif (method_exists($bao_name, 'add')) {
     $fct = 'add';
+    $fct_name = $bao_name . '::' . $fct;
+    $bao = call_user_func_array(array($bao_name, $fct), $args);
   }
-  if (!isset($fct)) {
-    return civicrm_api3_create_error('Entity not created, missing create or add method for ' . $bao_name);
+  else {
+    $fct_name = '_civicrm_api3_basic_create_fallback';
+    $bao = _civicrm_api3_basic_create_fallback($bao_name, $params);
   }
-  $bao = call_user_func_array(array($bao_name, $fct), $args);
+
   if (is_null($bao)) {
-    return civicrm_api3_create_error('Entity not created ' . $bao_name . '::' . $fct);
+    return civicrm_api3_create_error('Entity not created (' . $fct_name . ')');
   }
   else {
     $values = array();
     _civicrm_api3_object_to_array($bao, $values[$bao->id]);
-    return civicrm_api3_create_success($values, $params, NULL, 'create', $bao);
+    return civicrm_api3_create_success($values, $params, $entity, 'create', $bao);
   }
 }
 
+/**
+ * For BAO's which don't have a create() or add() functions, use this fallback implementation.
+ *
+ * @fixme There's an intuitive sense that this behavior should be defined somehow in the BAO/DAO class
+ * structure. In practice, that requires a fair amount of refactoring and/or kludgery.
+ *
+ * @param string $bao_name
+ * @param array $params
+ *
+ * @throws API_Exception
+ * @return CRM_Core_DAO|NULL an instance of the BAO
+ */
+function _civicrm_api3_basic_create_fallback($bao_name, &$params) {
+  $entityName = CRM_Core_DAO_AllCoreTables::getBriefName(get_parent_class($bao_name));
+  if (empty($entityName)) {
+    throw new API_Exception("Class \"$bao_name\" does not map to an entity name", "unmapped_class_to_entity", array(
+      'class_name' => $bao_name,
+    ));
+  }
+  $hook = empty($params['id']) ? 'create' : 'edit';
+
+  CRM_Utils_Hook::pre($hook, $entityName, CRM_Utils_Array::value('id', $params), $params);
+  $instance = new $bao_name();
+  $instance->copyValues($params);
+  $instance->save();
+  CRM_Utils_Hook::post($hook, $entityName, $instance->id, $instance);
+
+  return $instance;
+}
+
 /**
  * Function to do a 'standard' api del - when the api is only doing a $bao::del then use this
  * if api::del doesn't exist it will try DAO delete method
@@ -994,7 +1046,7 @@ function _civicrm_api3_basic_delete($bao_name, &$params) {
     if ($bao !== FALSE) {
       return civicrm_api3_create_success(TRUE);
     }
-    return civicrm_api3_create_error('Could not delete entity id ' . $params['id']);
+    throw new API_Exception('Could not delete entity id ' . $params['id']);
   }
   elseif (method_exists($bao_name, 'delete')) {
     $dao = new $bao_name();
@@ -1006,11 +1058,11 @@ function _civicrm_api3_basic_delete($bao_name, &$params) {
       }
     }
     else {
-      return civicrm_api3_create_error('Could not delete entity id ' . $params['id']);
+      throw new API_Exception('Could not delete entity id ' . $params['id']);
     }
   }
 
-  return civicrm_api3_create_error('no delete method found');
+  throw new API_Exception('no delete method found');
 }
 
 /**
@@ -1019,14 +1071,12 @@ function _civicrm_api3_basic_delete($bao_name, &$params) {
  *
  * @param array $returnArray - array to append custom data too - generally $result[4] where 4 is the entity id.
  * @param string $entity  e.g membership, event
+ * @param $entity_id
  * @param int $groupID - per CRM_Core_BAO_CustomGroup::getTree
  * @param int $subType e.g. membership_type_id where custom data doesn't apply to all membership types
  * @param string $subName - Subtype of entity
- *
  */
 function _civicrm_api3_custom_data_get(&$returnArray, $entity, $entity_id, $groupID = NULL, $subType = NULL, $subName = NULL) {
-  require_once 'CRM/Core/BAO/CustomGroup.php';
-  require_once 'CRM/Core/BAO/CustomField.php';
   $groupTree = &CRM_Core_BAO_CustomGroup::getTree($entity,
     CRM_Core_DAO::$_nullObject,
     $entity_id,
@@ -1064,56 +1114,52 @@ function _civicrm_api3_custom_data_get(&$returnArray, $entity, $entity_id, $grou
  * @param string $entity
  * @param string $action
  * @param array $params -
- * all variables are the same as per civicrm_api
+ * @param array $fields response from getfields all variables are the same as per civicrm_api
+ * @param bool $errorMode errorMode do intensive post fail checks?
+ * @throws Exception
  */
-function _civicrm_api3_validate_fields($entity, $action, &$params, $errorMode = NULL) {
-  //skip any entities without working getfields functions
-  $skippedEntities = array('entity', 'mailinggroup', 'customvalue', 'custom_value', 'mailing_group');
-  if (in_array(strtolower($entity), $skippedEntities) || strtolower($action) == 'getfields') {
-    return;
-  }
-  $fields = civicrm_api($entity, 'getfields', array('version' => 3, 'action' => $action));
-  $fields = array_intersect_key($fields['values'], $params);
-  foreach ($fields as $fieldname => $fieldInfo) {
+function _civicrm_api3_validate_fields($entity, $action, &$params, $fields, $errorMode = False) {
+  $fields = array_intersect_key($fields, $params);
+  foreach ($fields as $fieldName => $fieldInfo) {
     switch (CRM_Utils_Array::value('type', $fieldInfo)) {
       case CRM_Utils_Type::T_INT:
         //field is of type integer
-        _civicrm_api3_validate_integer($params, $fieldname, $fieldInfo, $entity);
+        _civicrm_api3_validate_integer($params, $fieldName, $fieldInfo, $entity);
         break;
 
       case 4:
       case 12:
         //field is of type date or datetime
-        _civicrm_api3_validate_date($params, $fieldname, $fieldInfo);
+        _civicrm_api3_validate_date($params, $fieldName, $fieldInfo);
         break;
 
-      case 32://blob
-        _civicrm_api3_validate_html($params, $fieldname, $fieldInfo);
+    case 32://blob
+        _civicrm_api3_validate_html($params, $fieldName, $fieldInfo);
         break;
 
       case CRM_Utils_Type::T_STRING:
-        _civicrm_api3_validate_string($params, $fieldname, $fieldInfo);
+        _civicrm_api3_validate_string($params, $fieldName, $fieldInfo, $entity);
         break;
 
       case CRM_Utils_Type::T_MONEY:
-        if (!CRM_Utils_Rule::money($params[$fieldname])) {
-          throw new Exception($fieldname . " is  not a valid amount: " . $params[$fieldname]);
+        if (!CRM_Utils_Rule::money($params[$fieldName]) && !empty($params[$fieldName])) {
+          throw new Exception($fieldName . " is  not a valid amount: " . $params[$fieldName]);
         }
     }
 
     // intensive checks - usually only called after DB level fail
     if (!empty($errorMode) && strtolower($action) == 'create') {
       if (CRM_Utils_Array::value('FKClassName', $fieldInfo)) {
-        if (CRM_Utils_Array::value($fieldname, $params)) {
-          _civicrm_api3_validate_constraint($params, $fieldname, $fieldInfo);
+        if (CRM_Utils_Array::value($fieldName, $params)) {
+          _civicrm_api3_validate_constraint($params, $fieldName, $fieldInfo);
         }
         elseif (CRM_Utils_Array::value('required', $fieldInfo)) {
-          throw new Exception("DB Constraint Violation - possibly $fieldname should possibly be marked as mandatory for this API. If so, please raise a bug report");
+          throw new Exception("DB Constraint Violation - possibly $fieldName should possibly be marked as mandatory for this API. If so, please raise a bug report");
         }
       }
       if (CRM_Utils_Array::value('api.unique', $fieldInfo)) {
         $params['entity'] = $entity;
-        _civicrm_api3_validate_uniquekey($params, $fieldname, $fieldInfo);
+        _civicrm_api3_validate_uniquekey($params, $fieldName, $fieldInfo);
       }
     }
   }
@@ -1130,10 +1176,12 @@ function _civicrm_api3_validate_fields($entity, $action, &$params, $errorMode =
  * may not be the best thing to do. There is no code level documentation on the existing functions to work off
  *
  * @param array $params params from civicrm_api
- * @param string $fieldname uniquename of field being checked
- * @param array $fieldinfo array of fields from getfields function
+ * @param string $fieldName uniquename of field being checked
+ * @param $fieldInfo
+ * @throws Exception
+ * @internal param array $fieldinfo array of fields from getfields function
  */
-function _civicrm_api3_validate_date(&$params, &$fieldname, &$fieldInfo) {
+function _civicrm_api3_validate_date(&$params, &$fieldName, &$fieldInfo) {
   //should we check first to prevent it from being copied if they have passed in sql friendly format?
   if (CRM_Utils_Array::value($fieldInfo['name'], $params)) {
     //accept 'whatever strtotime accepts
@@ -1142,12 +1190,12 @@ function _civicrm_api3_validate_date(&$params, &$fieldname, &$fieldInfo) {
     }
     $params[$fieldInfo['name']] = CRM_Utils_Date::processDate($params[$fieldInfo['name']]);
   }
-  if ((CRM_Utils_Array::value('name', $fieldInfo) != $fieldname) && CRM_Utils_Array::value($fieldname, $params)) {
+  if ((CRM_Utils_Array::value('name', $fieldInfo) != $fieldName) && CRM_Utils_Array::value($fieldName, $params)) {
     //If the unique field name differs from the db name & is set handle it here
-    if (strtotime($params[$fieldname]) === FALSE) {
-      throw new Exception($fieldname . " is not a valid date: " . $params[$fieldname]);
+    if (strtotime($params[$fieldName]) === FALSE) {
+      throw new Exception($fieldName . " is not a valid date: " . $params[$fieldName]);
     }
-    $params[$fieldname] = CRM_Utils_Date::processDate($params[$fieldname]);
+    $params[$fieldName] = CRM_Utils_Date::processDate($params[$fieldName]);
   }
 }
 
@@ -1155,18 +1203,18 @@ function _civicrm_api3_validate_date(&$params, &$fieldname, &$fieldInfo) {
  * Validate foreign constraint fields being passed into API.
  *
  * @param array $params params from civicrm_api
- * @param string $fieldname uniquename of field being checked
- * @param array $fieldinfo array of fields from getfields function
+ * @param string $fieldName uniquename of field being checked
+ * @param $fieldInfo
+ * @throws Exception
+ * @internal param array $fieldinfo array of fields from getfields function
  */
-function _civicrm_api3_validate_constraint(&$params, &$fieldname, &$fieldInfo) {
-  $file = str_replace('_', '/', $fieldInfo['FKClassName']) . ".php";
-  require_once ($file);
+function _civicrm_api3_validate_constraint(&$params, &$fieldName, &$fieldInfo) {
   $dao = new $fieldInfo['FKClassName'];
-  $dao->id = $params[$fieldname];
+  $dao->id = $params[$fieldName];
   $dao->selectAdd();
   $dao->selectAdd('id');
   if (!$dao->find()) {
-    throw new Exception("$fieldname is not valid : " . $params[$fieldname]);
+    throw new Exception("$fieldName is not valid : " . $params[$fieldName]);
   }
 }
 
@@ -1174,18 +1222,20 @@ function _civicrm_api3_validate_constraint(&$params, &$fieldname, &$fieldInfo) {
  * Validate foreign constraint fields being passed into API.
  *
  * @param array $params params from civicrm_api
- * @param string $fieldname uniquename of field being checked
- * @param array $fieldinfo array of fields from getfields function
+ * @param string $fieldName uniquename of field being checked
+ * @param $fieldInfo
+ * @throws Exception
+ * @internal param array $fieldinfo array of fields from getfields function
  */
-function _civicrm_api3_validate_uniquekey(&$params, &$fieldname, &$fieldInfo) {
+function _civicrm_api3_validate_uniquekey(&$params, &$fieldName, &$fieldInfo) {
   $existing = civicrm_api($params['entity'], 'get', array(
       'version' => $params['version'],
-      $fieldname => $params[$fieldname],
+      $fieldName => $params[$fieldName],
     ));
   // an entry already exists for this unique field
   if ($existing['count'] == 1) {
     // question - could this ever be a security issue?
-    throw new Exception("Field: `$fieldname` must be unique. An conflicting entity already exists - id: " . $existing['id']);
+    throw new Exception("Field: `$fieldName` must be unique. An conflicting entity already exists - id: " . $existing['id']);
   }
 }
 
@@ -1202,10 +1252,10 @@ function _civicrm_api3_validate_uniquekey(&$params, &$fieldname, &$fieldInfo) {
  * @param array $params params from civicrm_api, including:
  *   - 'values': an array of records to save
  *   - all other items: keys which identify new/pre-existing records
+ * @return array|int
  */
 function _civicrm_api3_generic_replace($entity, $params) {
 
-  require_once 'CRM/Core/Transaction.php';
   $transaction = new CRM_Core_Transaction();
   try {
     if (!is_array($params['values'])) {
@@ -1213,9 +1263,7 @@ function _civicrm_api3_generic_replace($entity, $params) {
     }
 
     // Extract the keys -- somewhat scary, don't think too hard about it
-    $baseParams = $params;
-    unset($baseParams['values']);
-    unset($baseParams['sequential']);
+    $baseParams = _civicrm_api3_generic_replace_base_params($params);
 
     // Lookup pre-existing records
     $preexisting = civicrm_api($entity, 'get', $baseParams, $params);
@@ -1268,10 +1316,27 @@ function _civicrm_api3_generic_replace($entity, $params) {
   }
 }
 
+/**
+ * @param $params
+ *
+ * @return mixed
+ */
+function _civicrm_api3_generic_replace_base_params($params) {
+  $baseParams = $params;
+  unset($baseParams['values']);
+  unset($baseParams['sequential']);
+  unset($baseParams['options']);
+  return $baseParams;
+}
+
 /**
  * returns fields allowable by api
+ *
  * @param $entity string Entity to query
  * @param bool $unique index by unique fields?
+ * @param array $params
+ *
+ * @return array
  */
 function _civicrm_api_get_fields($entity, $unique = FALSE, &$params = array(
   )) {
@@ -1280,8 +1345,6 @@ function _civicrm_api_get_fields($entity, $unique = FALSE, &$params = array(
   if (empty($dao)) {
     return array();
   }
-  $file = str_replace('_', '/', $dao) . ".php";
-  require_once ($file);
   $d = new $dao();
   $fields = $d->fields();
   // replace uniqueNames by the normal names as the key
@@ -1315,10 +1378,10 @@ function _civicrm_api_get_fields($entity, $unique = FALSE, &$params = array(
  * fields are prefixed with 'custom_' to represent api params
  */
 function _civicrm_api_get_custom_fields($entity, &$params) {
-  require_once 'CRM/Core/BAO/CustomField.php';
   $customfields = array();
   $entity = _civicrm_api_get_camel_name($entity);
   if (strtolower($entity) == 'contact') {
+    // Use sub-type if available, otherwise stick with 'Contact'
     $entity = CRM_Utils_Array::value('contact_type', $params);
   }
   $retrieveOnlyParent = FALSE;
@@ -1342,30 +1405,44 @@ function _civicrm_api_get_custom_fields($entity, &$params) {
   }
 
   foreach ($customfields as $key => $value) {
+    // Regular fields have a 'name' property
+    $value['name'] = 'custom_' . $key;
+    $value['type'] = _getStandardTypeFromCustomDataType($value['data_type']);
     $customfields['custom_' . $key] = $value;
-   if(in_array('custom_' . $key, $getoptions)){
-     $customfields['custom_' . $key]['options'] = CRM_Core_BAO_CustomOption::valuesByID($key);
-   }
+    if (in_array('custom_' . $key, $getoptions)) {
+      $customfields['custom_' . $key]['options'] = CRM_Core_BAO_CustomOption::valuesByID($key);
+    }
     unset($customfields[$key]);
   }
   return $customfields;
 }
-
+/**
+ * Translate the custom field data_type attribute into a std 'type'
+ */
+function _getStandardTypeFromCustomDataType($dataType) {
+  $mapping = array(
+    'String' => CRM_Utils_Type::T_STRING,
+    'Int' => CRM_Utils_Type::T_INT,
+    'Money' => CRM_Utils_Type::T_MONEY,
+    'Memo' => CRM_Utils_Type::T_LONGTEXT,
+    'Float' => CRM_Utils_Type::T_FLOAT,
+    'Date' => CRM_Utils_Type::T_DATE,
+    'Boolean' => CRM_Utils_Type::T_BOOLEAN,
+    'StateProvince' => CRM_Utils_Type::T_INT,
+    'File' => CRM_Utils_Type::T_STRING,
+    'Link' => CRM_Utils_Type::T_STRING,
+    'ContactReference' => CRM_Utils_Type::T_INT,
+    'Country' => CRM_Utils_Type::T_INT,
+  );
+  return $mapping[$dataType];
+}
 /**
  * Return array of defaults for the given API (function is a wrapper on getfields)
  */
-function _civicrm_api3_getdefaults($apiRequest) {
+function _civicrm_api3_getdefaults($apiRequest, $fields) {
   $defaults = array();
 
-  $result = civicrm_api($apiRequest['entity'],
-    'getfields',
-    array(
-      'version' => 3,
-      'action' => $apiRequest['action'],
-    )
-  );
-
-  foreach ($result['values'] as $field => $values) {
+  foreach ($fields as $field => $values) {
     if (isset($values['api.default'])) {
       $defaults[$field] = $values['api.default'];
     }
@@ -1376,17 +1453,10 @@ function _civicrm_api3_getdefaults($apiRequest) {
 /**
  * Return array of defaults for the given API (function is a wrapper on getfields)
  */
-function _civicrm_api3_getrequired($apiRequest) {
+function _civicrm_api3_getrequired($apiRequest, $fields) {
   $required = array('version');
 
-  $result = civicrm_api($apiRequest['entity'],
-    'getfields',
-    array(
-      'version' => 3,
-      'action' => $apiRequest['action'],
-    )
-  );
-  foreach ($result['values'] as $field => $values) {
+  foreach ($fields as $field => $values) {
     if (CRM_Utils_Array::value('api.required', $values)) {
       $required[] = $field;
     }
@@ -1400,23 +1470,8 @@ function _civicrm_api3_getrequired($apiRequest) {
  *
  * Function also swaps unique fields for non-unique fields & vice versa.
  */
-function _civicrm_api3_swap_out_aliases(&$apiRequest) {
-  if (strtolower($apiRequest['action'] == 'getfields')) {
-    if (CRM_Utils_Array::value('api_action', $apiRequest['params'])) {
-      $apiRequest['params']['action'] = $apiRequest['params']['api_action'];
-      unset($apiRequest['params']['api_action']);
-    }
-    return;
-  }
-  $result = civicrm_api($apiRequest['entity'],
-    'getfields',
-    array(
-      'version' => 3,
-      'action' => $apiRequest['action'],
-    )
-  );
-
-  foreach ($result['values'] as $field => $values) {
+function _civicrm_api3_swap_out_aliases(&$apiRequest, $fields) {
+  foreach ($fields as $field => $values) {
     $uniqueName = CRM_Utils_Array::value('uniqueName', $values);
     if (CRM_Utils_Array::value('api.aliases', $values)) {
       // if aliased field is not set we try to use field alias
@@ -1457,59 +1512,91 @@ function _civicrm_api3_swap_out_aliases(&$apiRequest) {
  * It currently converts the incoming value 'user_contact_id' into the id of the currenty logged in user
  *
  * @param array $params params from civicrm_api
- * @param string $fieldname uniquename of field being checked
- * @param array $fieldinfo array of fields from getfields function
+ * @param string $fieldName uniquename of field being checked
+ * @param $fieldInfo
+ * @param $entity
+ * @throws API_Exception
+ * @internal param array $fieldinfo array of fields from getfields function
  */
-function _civicrm_api3_validate_integer(&$params, &$fieldname, &$fieldInfo, $entity) {
+function _civicrm_api3_validate_integer(&$params, &$fieldName, &$fieldInfo, $entity) {
   //if fieldname exists in params
-  if (CRM_Utils_Array::value($fieldname, $params)) {
-    //if value = 'user_contact_id' replace value with logged in user id
-    if ($params[$fieldname] == "user_contact_id") {
-      $session = &CRM_Core_Session::singleton();
-      $params[$fieldname] = $session->get('userID');
-    }
-    if (CRM_Utils_Array::value('pseudoconstant', $fieldInfo) ) {
-      $constant = CRM_Utils_Array::value('options', $fieldInfo);
-      if (is_numeric($params[$fieldname]) && !CRM_Utils_Array::value('FKClassName',$fieldInfo) && !array_key_exists($params[$fieldname], $fieldInfo['options'])) {
-        throw new API_Exception("$fieldname is not valid", 2001, array('error_field' => $fieldname,"type"=>"integer"));
+  if (CRM_Utils_Array::value($fieldName, $params)) {
+    // if value = 'user_contact_id' (or similar), replace value with contact id
+    if (!is_numeric($params[$fieldName]) && is_scalar($params[$fieldName])) {
+      $realContactId = _civicrm_api3_resolve_contactID($params[$fieldName]);
+      if ('unknown-user' === $realContactId) {
+        throw new API_Exception("\"$fieldName\" \"{$params[$fieldName]}\" cannot be resolved to a contact ID", 2002, array('error_field' => $fieldName,"type"=>"integer"));
+      } elseif (is_numeric($realContactId)) {
+        $params[$fieldName] = $realContactId;
       }
     }
-    // we are looking for strings that should be swapped out e.g swap 'Donation' to financial_type_id 1
-    if (!is_numeric($params[$fieldname]) && !is_array($params[$fieldname])) {
-      if(CRM_Utils_Array::value('FKClassName', $fieldInfo)){
-        // we'll get the options for this now since we are doing a swap out
-        $options = civicrm_api($entity, 'getoptions', array('version' => 3, 'field' => $fieldname));
-        if(empty($options['is_error'])){
-          $fieldInfo['options'] = $options['values'];
-        }
-      }
-      if(!empty($fieldInfo['options'])){
-        $numericvalue = array_search($params[$fieldname], $fieldInfo['options']);
-        if (empty($numericvalue)) {
-          throw new Exception("$fieldname " . $params[$fieldname] . " is not valid");
-        }
-        else {
-          $params[$fieldname] = $numericvalue;
-        }
+    if (!empty($fieldInfo['pseudoconstant']) || !empty($fieldInfo['options'])) {
+      _civicrm_api3_api_match_pseudoconstant($params, $entity, $fieldName, $fieldInfo);
+    }
+
+    // After swapping options, ensure we have an integer(s)
+    foreach ((array) ($params[$fieldName]) as $value) {
+      if ($value && !is_numeric($value) && $value !== 'null' && !is_array($value)) {
+        throw new API_Exception("$fieldName is not a valid integer", 2001, array('error_field' => $fieldName, "type" => "integer"));
       }
     }
 
-    // once we have done any swaps check our field length
-    if(is_string($params[$fieldname]) &&
+    // Check our field length
+    if(is_string($params[$fieldName]) &&
       CRM_Utils_Array::value('maxlength',$fieldInfo)
-      && strlen($params[$fieldname]) > $fieldInfo['maxlength']
+      && strlen($params[$fieldName]) > $fieldInfo['maxlength']
       ){
-      throw new API_Exception( $params[$fieldname] . " is " . strlen($params[$fieldname]) . " characters  - longer than $fieldname length" . $fieldInfo['maxlength'] . ' characters',
-        2100, array('field' => $fieldname, "max_length"=>$fieldInfo['maxlength'])
+      throw new API_Exception( $params[$fieldName] . " is " . strlen($params[$fieldName]) . " characters  - longer than $fieldName length" . $fieldInfo['maxlength'] . ' characters',
+        2100, array('field' => $fieldName, "max_length"=>$fieldInfo['maxlength'])
       );
     }
   }
 }
 
-function _civicrm_api3_validate_html(&$params, &$fieldname, &$fieldInfo) {
-  if ($value = CRM_Utils_Array::value($fieldname, $params)) {
+/**
+ * Determine a contact ID using a string expression
+ *
+ * @param string $contactIdExpr e.g. "user_contact_id" or "@user:username"
+ * @return int|NULL|'unknown-user'
+ */
+function  _civicrm_api3_resolve_contactID($contactIdExpr) {
+  //if value = 'user_contact_id' replace value with logged in user id
+  if ($contactIdExpr == "user_contact_id") {
+    $session = &CRM_Core_Session::singleton();
+    if (!is_numeric($session->get('userID'))) {
+      return NULL;
+    }
+    return $session->get('userID');
+  } elseif (preg_match('/^@user:(.*)$/', $contactIdExpr, $matches)) {
+    $config = CRM_Core_Config::singleton();
+
+    $ufID = $config->userSystem->getUfId($matches[1]);
+    if (!$ufID) {
+      return 'unknown-user';
+    }
+
+    $contactID = CRM_Core_BAO_UFMatch::getContactId($ufID);
+    if (!$contactID) {
+      return 'unknown-user';
+    }
+
+    return $contactID;
+  }
+  return NULL;
+}
+
+/**
+ * Validate html (check for scripting attack)
+ * @param $params
+ * @param $fieldName
+ * @param $fieldInfo
+ *
+ * @throws API_Exception
+ */
+function _civicrm_api3_validate_html(&$params, &$fieldName, &$fieldInfo) {
+  if ($value = CRM_Utils_Array::value($fieldName, $params)) {
     if (!CRM_Utils_Rule::xssString($value)) {
-      throw new API_Exception('Illegal characters in input (potential scripting attack)',array("field"=>$fieldname,"error_code"=>"xss"));
+      throw new API_Exception('Illegal characters in input (potential scripting attack)', array("field"=>$fieldName,"error_code"=>"xss"));
     }
   }
 }
@@ -1517,38 +1604,151 @@ function _civicrm_api3_validate_html(&$params, &$fieldname, &$fieldInfo) {
 /**
  * Validate string fields being passed into API.
  * @param array $params params from civicrm_api
- * @param string $fieldname uniquename of field being checked
- * @param array $fieldinfo array of fields from getfields function
+ * @param string $fieldName uniquename of field being checked
+ * @param $fieldInfo
+ * @param $entity
+ * @throws API_Exception
+ * @throws Exception
+ * @internal param array $fieldinfo array of fields from getfields function
  */
-function _civicrm_api3_validate_string(&$params, &$fieldname, &$fieldInfo) {
+function _civicrm_api3_validate_string(&$params, &$fieldName, &$fieldInfo, $entity) {
   // If fieldname exists in params
-  $value = (string) CRM_Utils_Array::value($fieldname, $params,'');
+  $value = CRM_Utils_Array::value($fieldName, $params, '');
+  if(!is_array($value)){
+    $value = (string) $value;
+  }
+  else{
+    //@todo what do we do about passed in arrays. For many of these fields
+    // the missing piece of functionality is separating them to a separated string
+    // & many save incorrectly. But can we change them wholesale?
+  }
   if ($value ) {
     if (!CRM_Utils_Rule::xssString($value)) {
       throw new Exception('Illegal characters in input (potential scripting attack)');
     }
-    if ($fieldname == 'currency') {
+    if ($fieldName == 'currency') {
       if (!CRM_Utils_Rule::currencyCode($value)) {
         throw new Exception("Currency not a valid code: $value");
       }
     }
-    if (!empty ($fieldInfo['options'])) {
-      // Validate & swap out any pseudoconstants / options
-      $options = $fieldInfo['options'];
-      $lowerCaseOptions = array_map("strtolower", $options);
-      // If value passed is not a key, it may be a label
-      // Try to lookup key from label - if it can't be found throw error
-      if (!isset($options[strtolower($value)]) && !isset($options[$value]) ) {
-        if (!in_array(strtolower($value), $lowerCaseOptions)) {
-          throw new Exception("$fieldname `$value` is not valid.");
-        }
-      }
+    if (!empty($fieldInfo['pseudoconstant']) || !empty($fieldInfo['options']) || !empty($fieldInfo['enumValues'])) {
+      _civicrm_api3_api_match_pseudoconstant($params, $entity, $fieldName, $fieldInfo);
     }
     // Check our field length
     elseif (is_string($value) && !empty($fieldInfo['maxlength']) && strlen($value) > $fieldInfo['maxlength']) {
-      throw new API_Exception("Value for $fieldname is " . strlen($value) . " characters  - This field has a maxlength of {$fieldInfo['maxlength']} characters.",
-        2100, array('field' => $fieldname)
+      throw new API_Exception("Value for $fieldName is " . strlen($value) . " characters  - This field has a maxlength of {$fieldInfo['maxlength']} characters.",
+        2100, array('field' => $fieldName)
       );
     }
   }
 }
+
+/**
+ * Validate & swap out any pseudoconstants / options
+ *
+ * @param $params: api parameters
+ * @param $entity: api entity name
+ * @param $fieldName: field name used in api call (not necessarily the canonical name)
+ * @param $fieldInfo: getfields meta-data
+ */
+function _civicrm_api3_api_match_pseudoconstant(&$params, $entity, $fieldName, $fieldInfo) {
+  $options = CRM_Utils_Array::value('options', $fieldInfo);
+  if (!$options) {
+    if(strtolower($entity) == 'profile' && !empty($fieldInfo['entity'])) {
+      // we need to get the options from the entity the field relates to
+      $entity = $fieldInfo['entity'];
+    }
+    $options = civicrm_api($entity, 'getoptions', array('version' => 3, 'field' => $fieldInfo['name'], 'context' => 'validate'));
+    $options = CRM_Utils_Array::value('values', $options, array());
+  }
+
+  // If passed a value-separated string, explode to an array, then re-implode after matching values
+  $implode = FALSE;
+  if (is_string($params[$fieldName]) && strpos($params[$fieldName], CRM_Core_DAO::VALUE_SEPARATOR) !== FALSE) {
+    $params[$fieldName] = CRM_Utils_Array::explodePadded($params[$fieldName]);
+    $implode = TRUE;
+  }
+  // If passed multiple options, validate each
+  if (is_array($params[$fieldName])) {
+    foreach ($params[$fieldName] as &$value) {
+      if (!is_array($value)) {
+        _civicrm_api3_api_match_pseudoconstant_value($value, $options, $fieldName);
+      }
+    }
+    // TODO: unwrap the call to implodePadded from the conditional and do it always
+    // need to verify that this is safe and doesn't break anything though.
+    // Better yet would be to leave it as an array and ensure that every dao/bao can handle array input
+    if ($implode) {
+      CRM_Utils_Array::implodePadded($params[$fieldName]);
+    }
+  }
+  else {
+    _civicrm_api3_api_match_pseudoconstant_value($params[$fieldName], $options, $fieldName);
+  }
+}
+
+/**
+ * Validate & swap a single option value for a field
+ *
+ * @param $value: field value
+ * @param $options: array of options for this field
+ * @param $fieldName: field name used in api call (not necessarily the canonical name)
+ * @throws API_Exception
+ */
+function _civicrm_api3_api_match_pseudoconstant_value(&$value, $options, $fieldName) {
+  // If option is a key, no need to translate
+  if (array_key_exists($value, $options)) {
+    return;
+  }
+
+  // Translate value into key
+  $newValue = array_search($value, $options);
+  if ($newValue !== FALSE) {
+    $value = $newValue;
+    return;
+  }
+  // Case-insensitive matching
+  $newValue = strtolower($value);
+  $options = array_map("strtolower", $options);
+  $newValue = array_search($newValue, $options);
+  if ($newValue === FALSE) {
+    throw new API_Exception("'$value' is not a valid option for field $fieldName", 2001, array('error_field' => $fieldName));
+  }
+  $value = $newValue;
+}
+
+/**
+ * Returns the canonical name of a field
+ * @param $entity: api entity name (string should already be standardized - no camelCase)
+ * @param $fieldName: any variation of a field's name (name, unique_name, api.alias)
+ *
+ * @return (string|bool) fieldName or FALSE if the field does not exist
+ */
+function _civicrm_api3_api_resolve_alias($entity, $fieldName) {
+  if (strpos($fieldName, 'custom_') === 0 && is_numeric($fieldName[7])) {
+    return $fieldName;
+  }
+  if ($fieldName == "{$entity}_id") {
+    return 'id';
+  }
+  $result = civicrm_api($entity, 'getfields', array(
+    'version' => 3,
+    'action' => 'create',
+  ));
+  $meta = $result['values'];
+  if (!isset($meta[$fieldName]['name']) && isset($meta[$fieldName . '_id'])) {
+    $fieldName = $fieldName . '_id';
+  }
+  if (isset($meta[$fieldName])) {
+    return $meta[$fieldName]['name'];
+  }
+  foreach ($meta as $info) {
+    if ($fieldName == CRM_Utils_Array::value('uniqueName', $info)) {
+      return $info['name'];
+    }
+    if (array_search($fieldName, CRM_Utils_Array::value('api.aliases', $info, array())) !== FALSE) {
+      return $info['name'];
+    }
+  }
+  return FALSE;
+}