Merge pull request #1984 from dlobo/CRM-13621
[civicrm-core.git] / api / v3 / utils.php
index 84b7f24dc2a035069159f9bfa22b650870d61642..36e84b908049ef1351e0822e90be48a1188d283b 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 /*
   +--------------------------------------------------------------------+
-  | CiviCRM version 4.3                                                |
+  | CiviCRM version 4.4                                                |
   +--------------------------------------------------------------------+
   | Copyright CiviCRM LLC (c) 2004-2013                                |
   +--------------------------------------------------------------------+
@@ -50,11 +50,11 @@ function _civicrm_api3_initialize() {
  *
  * @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(
   )) {
@@ -73,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
@@ -127,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
@@ -142,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();
@@ -187,8 +190,13 @@ 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(!empty($item['next_sched_contribution_date'])){
+        // 4.4 legacy handling
+        $values[$key]['next_sched_contribution'] = $item['next_sched_contribution_date'];
+      }
     }
   }
 
@@ -261,9 +269,10 @@ function _civicrm_api3_load_DAO($entity) {
 
 /**
  * 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) {
   if (strpos($name, 'civicrm_api3') !== FALSE) {
@@ -291,9 +300,10 @@ function _civicrm_api3_get_DAO($name) {
 
 /**
  * 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);
@@ -358,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
@@ -366,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){
 
@@ -420,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,
@@ -554,9 +582,14 @@ 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 = '') {
@@ -593,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']);
     }
@@ -606,15 +639,18 @@ function _civicrm_api3_get_options_from_params(&$params, $queryObject = FALSE, $
     }
   }
 
-
   $options = array(
-    'offset' => $offset,
-    'sort' => $sort,
-    'limit' => $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;
   }
@@ -629,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{
+    else {
       $inputParams[$n] = $v;
+      if ($v && !is_array($v) && stristr($v, 'SELECT')) {
+        throw new API_Exception('invalid string');
+      }
     }
   }
   $options['return'] = array_merge($returnProperties, $legacyreturnProperties);
@@ -644,8 +683,10 @@ 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) {
 
@@ -694,8 +735,15 @@ 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
  */
@@ -746,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
@@ -800,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)
@@ -861,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) {
@@ -896,6 +948,9 @@ 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();
@@ -913,6 +968,7 @@ 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) {
 
@@ -950,11 +1006,13 @@ function _civicrm_api3_basic_create($bao_name, &$params, $entity = NULL) {
 /**
  * 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
+ * @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) {
@@ -1013,10 +1071,10 @@ 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) {
   $groupTree = &CRM_Core_BAO_CustomGroup::getTree($entity,
@@ -1056,16 +1114,12 @@ 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);
+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:
@@ -1088,7 +1142,7 @@ function _civicrm_api3_validate_fields($entity, $action, &$params, $errorMode =
         break;
 
       case CRM_Utils_Type::T_MONEY:
-        if (!CRM_Utils_Rule::money($params[$fieldName])) {
+        if (!CRM_Utils_Rule::money($params[$fieldName]) && !empty($params[$fieldName])) {
           throw new Exception($fieldName . " is  not a valid amount: " . $params[$fieldName]);
         }
     }
@@ -1123,7 +1177,9 @@ function _civicrm_api3_validate_fields($entity, $action, &$params, $errorMode =
  *
  * @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 $fieldInfo
+ * @throws Exception
+ * @internal param array $fieldinfo array of fields from getfields function
  */
 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?
@@ -1148,7 +1204,9 @@ function _civicrm_api3_validate_date(&$params, &$fieldName, &$fieldInfo) {
  *
  * @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 $fieldInfo
+ * @throws Exception
+ * @internal param array $fieldinfo array of fields from getfields function
  */
 function _civicrm_api3_validate_constraint(&$params, &$fieldName, &$fieldInfo) {
   $dao = new $fieldInfo['FKClassName'];
@@ -1165,7 +1223,9 @@ function _civicrm_api3_validate_constraint(&$params, &$fieldName, &$fieldInfo) {
  *
  * @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 $fieldInfo
+ * @throws Exception
+ * @internal param array $fieldinfo array of fields from getfields function
  */
 function _civicrm_api3_validate_uniquekey(&$params, &$fieldName, &$fieldInfo) {
   $existing = civicrm_api($params['entity'], 'get', array(
@@ -1192,6 +1252,7 @@ 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) {
 
@@ -1202,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);
@@ -1257,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(
   )) {
@@ -1305,6 +1381,7 @@ function _civicrm_api_get_custom_fields($entity, &$params) {
   $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;
@@ -1330,6 +1407,7 @@ 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);
@@ -1338,22 +1416,33 @@ function _civicrm_api_get_custom_fields($entity, &$params) {
   }
   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'];
     }
@@ -1364,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;
     }
@@ -1388,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
@@ -1446,7 +1513,10 @@ function _civicrm_api3_swap_out_aliases(&$apiRequest) {
  *
  * @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 $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) {
   //if fieldname exists in params
@@ -1515,6 +1585,14 @@ function  _civicrm_api3_resolve_contactID($contactIdExpr) {
   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)) {
@@ -1527,7 +1605,11 @@ 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 $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, $entity) {
   // If fieldname exists in params
@@ -1572,11 +1654,15 @@ function _civicrm_api3_validate_string(&$params, &$fieldName, &$fieldInfo, $enti
 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-seperated string, explode to an array, then re-implode after matching values
+  // 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]);
@@ -1607,6 +1693,7 @@ function _civicrm_api3_api_match_pseudoconstant(&$params, $entity, $fieldName, $
  * @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