Merge pull request #3679 from yashodha/CRM-14951
[civicrm-core.git] / api / v3 / utils.php
index 8acc21dfc5b460772846a5ae125fcde74b88a05c..3fb9699444568f795502c324bd11f4cb4696ecae 100644 (file)
@@ -50,9 +50,8 @@ function _civicrm_api3_initialize() {
  *
  * @param array $params array of fields to checkl
  * @param array $daoName string DAO to check for required fields (create functions only)
- * @param array $keyoptions
+ * @param array $keyoptions list of required fields options. One of the options is required
  *
- * @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
  * @
  */
@@ -151,7 +150,7 @@ function civicrm_api3_create_error($msg, $data = array()) {
 /**
  * Format array in result output styple
  *
- * @param array $values values generated by API operation (the result)
+ * @param array|int $values values generated by API operation (the result)
  * @param array $params parameters passed into API call
  * @param string $entity the entity being acted on
  * @param string $action the action passed to the API
@@ -308,6 +307,7 @@ function _civicrm_api3_get_DAO($name) {
   if(file_exists("api/v3/$name.php")) {
     include_once "api/v3/$name.php";
   }
+
   $daoFn = "_civicrm_api3_" . _civicrm_api_get_entity_name_from_camel($name) . "_DAO";
   if (function_exists($daoFn)) {
     return $daoFn();
@@ -408,11 +408,10 @@ function _civicrm_api3_store_values(&$fields, &$params, &$values) {
  * 2 variants call
  * @param $entity
  * @param array $params as passed into api get or getcount function
- * @param array $additional_options
+ * @param array $additional_options array of options (so we can modify the filter)
  * @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){
 
@@ -538,6 +537,13 @@ function _civicrm_api3_get_query_object($params, $mode, $entity) {
 
 /**
  * Function transfers the filters being passed into the DAO onto the params object
+ * @param CRM_Core_DAO $dao
+ * @param array $params
+ * @param bool $unique
+ * @param string $entity
+ *
+ * @throws API_Exception
+ * @throws Exception
  */
 function _civicrm_api3_dao_set_filter(&$dao, $params, $unique = TRUE, $entity) {
   $entity = substr($dao->__table, 8);
@@ -657,6 +663,7 @@ function _civicrm_api3_apply_filters_to_dao($filterField, $filterValue, &$dao) {
  * @param string $entity
  * @param string $action
  *
+ * @throws API_Exception
  * @return array $options options extracted from params
  */
 function _civicrm_api3_get_options_from_params(&$params, $queryObject = FALSE, $entity = '', $action = '') {
@@ -792,6 +799,9 @@ function _civicrm_api3_build_fields_array(&$bao, $unique = TRUE) {
 /**
  * build fields array. This is the array of fields as it relates to the given DAO
  * returns unique fields as keys by default but if set but can return by DB fields
+ * @param CRM_Core_BAO $bao
+ *
+ * @return mixed
  */
 function _civicrm_api3_get_unique_name_array(&$bao) {
   $fields = $bao->fields();
@@ -915,9 +925,25 @@ function _civicrm_api3_object_to_array_unique_fields(&$dao, &$values) {
  */
 function _civicrm_api3_custom_format_params($params, &$values, $extends, $entityId = NULL) {
   $values['custom'] = array();
+  $checkCheckBoxField = FALSE;
+  $entity = $extends;
+  if(in_array($extends, array('Household', 'Individual', 'Organization'))) {
+    $entity = 'Contact';
+  }
+
+  $fields = civicrm_api($entity, 'getfields', array('version' => 3, 'action' => 'create'));
+  if(!$fields['is_error']) {
+    // not sure if fields could be error - maybe change to using civicrm_api3 wrapper later - this is conservative
+    $fields = $fields['values'];
+    $checkCheckBoxField = TRUE;
+  }
+
   foreach ($params as $key => $value) {
     list($customFieldID, $customValueID) = CRM_Core_BAO_CustomField::getKeyID($key, TRUE);
     if ($customFieldID && (!IS_NULL($value))) {
+      if ($checkCheckBoxField && !empty($fields['custom_' . $customFieldID]) && $fields['custom_' . $customFieldID]['html_type'] == 'CheckBox') {
+        formatCheckBoxField($value, 'custom_' . $customFieldID, $entity);
+      }
       CRM_Core_BAO_CustomField::formatCustomField($customFieldID, $values['custom'],
         $value, $extends, $customValueID, $entityId, FALSE, FALSE
       );
@@ -941,6 +967,101 @@ function _civicrm_api3_format_params_for_create(&$params, $entity) {
   $params = array_merge($params, $values);
 }
 
+/**
+ * we can't rely on downstream to add separators to checkboxes so we'll check here. We should look at pushing to BAO function
+ * and / or validate function but this is a safe place for now as it has massive test coverage & we can keep the change very specific
+ * note that this is specifically tested in the GRANT api test case so later refactoring should use that as a checking point
+ *
+ * We will only alter the value if we are sure that changing it will make it correct - if it appears wrong but does not appear to have a clear fix we
+ * don't touch - lots of very cautious code in here
+ *
+ * The resulting array should look like
+ * array(
+ *  'key' => 1,
+ *  'key1' => 1,
+ * );
+ *
+ * OR one or more keys wrapped in a CRM_Core_DAO::VALUE_SEPARATOR - either it accepted by the receiving function
+ *
+ * @todo - we are probably skipping handling disabled options as presumably getoptions is not giving us them. This should be non-regressive but might
+ * be fixed in future
+ *
+ * @param $checkboxFieldValue
+ * @param $customFieldLabel
+ * @param $entity
+ *
+ */
+function formatCheckBoxField(&$checkboxFieldValue, $customFieldLabel, $entity) {
+
+  if (is_string($checkboxFieldValue) && stristr($checkboxFieldValue, CRM_Core_DAO::VALUE_SEPARATOR)) {
+    // we can assume it's pre-formatted
+    return;
+  }
+  $options = civicrm_api($entity, 'getoptions', array('field' => $customFieldLabel, 'version' => 3));
+  if (!empty($options['is_error'])) {
+    //the check is precautionary - can probably be removed later
+    return;
+  }
+
+  $options = $options['values'];
+  $validValue = TRUE;
+  if (is_array($checkboxFieldValue)) {
+    foreach ($checkboxFieldValue as $key => $value) {
+      if (!array_key_exists($key, $options)) {
+        $validValue = FALSE;
+      }
+    }
+    if ($validValue) {
+      // we have been passed an array that is already in the 'odd' custom field format
+      return;
+    }
+  }
+
+  // so we either have an array that is not keyed by the value or we have a string that doesn't hold separators
+  // if the array only has one item we'll treat it like any other string
+  if (is_array($checkboxFieldValue) && count($checkboxFieldValue) == 1) {
+    $possibleValue = reset($checkboxFieldValue);
+  }
+  if (is_string($checkboxFieldValue)) {
+    $possibleValue = $checkboxFieldValue;
+  }
+  if (isset($possibleValue) && array_key_exists($possibleValue, $options)) {
+    $checkboxFieldValue = CRM_Core_DAO::VALUE_SEPARATOR . $possibleValue . CRM_Core_DAO::VALUE_SEPARATOR;
+    return;
+  }
+  elseif (is_array($checkboxFieldValue)) {
+    // so this time around we are considering the values in the array
+    $possibleValues = $checkboxFieldValue;
+    $formatValue = TRUE;
+  }
+  elseif (stristr($checkboxFieldValue, ',')) {
+    $formatValue = TRUE;
+    //lets see if we should separate it - we do this near the end so we
+    // ensure we have already checked that the comma is not part of a legitimate match
+    // and of course, we don't make any changes if we don't now have matches
+    $possibleValues = explode(',', $checkboxFieldValue);
+  }
+  else {
+    // run out of ideas as to what the format might be - if it's a string it doesn't match with or without the ','
+    return;
+  }
+
+  foreach ($possibleValues as $index => $possibleValue) {
+    if (array_key_exists($possibleValue, $options)) {
+      // do nothing - we will leave formatValue set to true unless another value is not found (which would cause us to ignore the whole value set)
+    }
+    elseif (array_key_exists(trim($possibleValue), $options)) {
+      $possibleValues[$index] = trim($possibleValue);
+    }
+    else {
+      $formatValue = FALSE;
+    }
+  }
+  if ($formatValue) {
+    $checkboxFieldValue = CRM_Core_DAO::VALUE_SEPARATOR . implode(CRM_Core_DAO::VALUE_SEPARATOR, $possibleValues) . CRM_Core_DAO::VALUE_SEPARATOR;
+  }
+}
+
 /**
  * @deprecated
  * This function ensures that we have the right input parameters
@@ -1030,9 +1151,12 @@ function _civicrm_api3_basic_get($bao_name, &$params, $returnAsSuccess = TRUE, $
 
 /**
  * Function to do a 'standard' api create - when the api is only doing a $bao::create then use this
+ *
  * @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
+ *
+ * @throws API_Exception
  * @return array
  */
 function _civicrm_api3_basic_create($bao_name, &$params, $entity = NULL) {
@@ -1115,6 +1239,12 @@ function _civicrm_api3_basic_create_fallback($bao_name, &$params) {
 /**
  * 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
+ *
+ * @param $bao_name
+ * @param $params
+ *
+ * @return array API result array
+ * @throws API_Exception
  */
 function _civicrm_api3_basic_delete($bao_name, &$params) {
 
@@ -1166,19 +1296,23 @@ function _civicrm_api3_custom_data_get(&$returnArray, $entity, $entity_id, $grou
   $groupTree = CRM_Core_BAO_CustomGroup::formatGroupTree($groupTree, 1, CRM_Core_DAO::$_nullObject);
   $customValues = array();
   CRM_Core_BAO_CustomGroup::setDefaults($groupTree, $customValues);
+  $fieldInfo = array();
+  foreach ($groupTree as $set) {
+    $fieldInfo += $set['fields'];
+  }
   if (!empty($customValues)) {
     foreach ($customValues as $key => $val) {
-      if (strstr($key, '_id')) {
-        $idkey = substr($key, 0, -3);
-        $returnArray['custom_' . (CRM_Core_BAO_CustomField::getKeyID($idkey) . "_id")] = $val;
-        $returnArray[$key] = $val;
-      }
-      else {
-        // per standard - return custom_fieldID
-        $returnArray['custom_' . (CRM_Core_BAO_CustomField::getKeyID($key))] = $val;
+      // per standard - return custom_fieldID
+      $id = CRM_Core_BAO_CustomField::getKeyID($key);
+      $returnArray['custom_' . $id] = $val;
+
+      //not standard - but some api did this so guess we should keep - cheap as chips
+      $returnArray[$key] = $val;
 
-        //not standard - but some api did this so guess we should keep - cheap as chips
-        $returnArray[$key] = $val;
+      // Shim to restore legacy behavior of ContactReference custom fields
+      if (!empty($fieldInfo[$id]) && $fieldInfo[$id]['data_type'] == 'ContactReference') {
+        $returnArray['custom_' . $id . '_id'] = $returnArray[$key . '_id'] = $val;
+        $returnArray['custom_' . $id] = $returnArray[$key] = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact', $val, 'sort_name');
       }
     }
   }
@@ -1208,6 +1342,7 @@ function _civicrm_api3_validate_fields($entity, $action, &$params, $fields, $err
 
       case 4:
       case 12:
+      case CRM_Utils_Type::T_TIMESTAMP:
         //field is of type date or datetime
         _civicrm_api3_validate_date($params, $fieldName, $fieldInfo);
         break;
@@ -1256,27 +1391,45 @@ function _civicrm_api3_validate_fields($entity, $action, &$params, $fields, $err
  *
  * @param array $params params from civicrm_api
  * @param string $fieldName uniquename of field being checked
- * @param $fieldInfo
+ * @param array $fieldInfo array of fields from getfields function
  * @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?
   if (!empty($params[$fieldInfo['name']])) {
-    //accept 'whatever strtotime accepts
-    if (strtotime($params[$fieldInfo['name']]) === FALSE) {
-      throw new Exception($fieldInfo['name'] . " is not a valid date: " . $params[$fieldInfo['name']]);
-    }
-    $format = ($fieldInfo['type'] == CRM_Utils_Type::T_DATE) ? 'Ymd000000' : 'YmdHis';
-    $params[$fieldInfo['name']] = CRM_Utils_Date::processDate($params[$fieldInfo['name']], NULL, FALSE, $format);
+    $params[$fieldInfo['name']] = _civicrm_api3_getValidDate($params[$fieldInfo['name']], $fieldInfo['name'], $fieldInfo['type']);
   }
   if ((CRM_Utils_Array::value('name', $fieldInfo) != $fieldName) && !empty($params[$fieldName])) {
-    //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]);
+    $params[$fieldName] = _civicrm_api3_getValidDate($params[$fieldName], $fieldName, $fieldInfo['type']);
+  }
+}
+
+/**
+ * convert date into BAO friendly date
+ * we accept 'whatever strtotime accepts'
+ *
+ * @param string $dateValue
+ * @param $fieldName
+ * @param $fieldType
+ *
+ * @throws Exception
+ * @internal param $fieldInfo
+ *
+ * @internal param $params
+ * @return mixed
+ */
+function _civicrm_api3_getValidDate($dateValue, $fieldName, $fieldType) {
+  if (is_array($dateValue)) {
+    foreach ($dateValue as $key => $value) {
+      $dateValue[$key] = _civicrm_api3_getValidDate($value, $fieldName, $fieldType);
     }
-    $params[$fieldName] = CRM_Utils_Date::processDate($params[$fieldName]);
+    return $dateValue;
+  }
+  if (strtotime($dateValue) === FALSE) {
+    throw new Exception($fieldName . " is not a valid date: " . $dateValue);
   }
+  $format = ($fieldType == CRM_Utils_Type::T_DATE) ? 'Ymd000000' : 'YmdHis';
+  return CRM_Utils_Date::processDate($dateValue, NULL, FALSE, $format);
 }
 
 /**
@@ -1284,9 +1437,8 @@ function _civicrm_api3_validate_date(&$params, &$fieldName, &$fieldInfo) {
  *
  * @param array $params params from civicrm_api
  * @param string $fieldName uniquename of field being checked
- * @param $fieldInfo
+ * @param array $fieldInfo array of fields from getfields function
  * @throws Exception
- * @internal param array $fieldinfo array of fields from getfields function
  */
 function _civicrm_api3_validate_constraint(&$params, &$fieldName, &$fieldInfo) {
   $dao = new $fieldInfo['FKClassName'];
@@ -1303,9 +1455,8 @@ function _civicrm_api3_validate_constraint(&$params, &$fieldName, &$fieldInfo) {
  *
  * @param array $params params from civicrm_api
  * @param string $fieldName uniquename of field being checked
- * @param $fieldInfo
+ * @param $fieldInfo array of fields from getfields function
  * @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(
@@ -1558,10 +1709,9 @@ function _civicrm_api3_swap_out_aliases(&$apiRequest, $fields) {
  *
  * @param array $params params from civicrm_api
  * @param string $fieldName uniquename of field being checked
- * @param $fieldInfo
- * @param $entity
+ * @param array $fieldInfo array of fields from getfields function
+ * @param string $entity
  * @throws API_Exception
- * @internal param array $fieldinfo array of fields from getfields function
  */
 function _civicrm_api3_validate_integer(&$params, &$fieldName, &$fieldInfo, $entity) {
   if (!empty($params[$fieldName])) {
@@ -1626,13 +1776,13 @@ function  _civicrm_api3_resolve_contactID($contactIdExpr) {
 
 /**
  * Validate html (check for scripting attack)
- * @param $params
- * @param $fieldName
- * @param $fieldInfo
+ * @param array $params
+ * @param string $fieldName
+ * @param array $fieldInfo
  *
  * @throws API_Exception
  */
-function _civicrm_api3_validate_html(&$params, &$fieldName, &$fieldInfo) {
+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"));
@@ -1644,11 +1794,10 @@ 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 $fieldInfo
- * @param $entity
+ * @param array $fieldInfo array of fields from getfields function
+ * @param string $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
@@ -1674,8 +1823,8 @@ function _civicrm_api3_validate_string(&$params, &$fieldName, &$fieldInfo, $enti
       _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.",
+    elseif (is_string($value) && !empty($fieldInfo['maxlength']) && strlen(utf8_decode($value)) > $fieldInfo['maxlength']) {
+      throw new API_Exception("Value for $fieldName is " . strlen(utf8_decode($value)) . " characters  - This field has a maxlength of {$fieldInfo['maxlength']} characters.",
         2100, array('field' => $fieldName)
       );
     }
@@ -1685,10 +1834,10 @@ function _civicrm_api3_validate_string(&$params, &$fieldName, &$fieldInfo, $enti
 /**
  * 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
+ * @param array $params: api parameters
+ * @param string $entity: api entity name
+ * @param string $fieldName: field name used in api call (not necessarily the canonical name)
+ * @param array $fieldInfo: getfields meta-data
  */
 function _civicrm_api3_api_match_pseudoconstant(&$params, $entity, $fieldName, $fieldInfo) {
   $options = CRM_Utils_Array::value('options', $fieldInfo);
@@ -1729,9 +1878,9 @@ function _civicrm_api3_api_match_pseudoconstant(&$params, $entity, $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)
+ * @param string $value: field value
+ * @param array $options: array of options for this field
+ * @param string $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) {
@@ -1758,10 +1907,11 @@ function _civicrm_api3_api_match_pseudoconstant_value(&$value, $options, $fieldN
 
 /**
  * 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
+ * @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 bool|string (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])) {