}
}
}
- //if ( array_key_exists ('debug',$params) && is_object ($dao)) {
- if (is_array($params) && array_key_exists('debug', $params)) {
+
+ if (is_array($params) && !empty($params['debug'])) {
if (is_string($action) && $action != 'getfields') {
$apiFields = civicrm_api($entity, 'getfields', array('version' => 3, 'action' => $action) + $params);
}
$name = 'Contact';
}
- //hack to deal with incorrectly named BAO/DAO - see CRM-10859 - remove after rename
- if($name == 'price_set' || $name == 'PriceSet'){
- return 'CRM_Price_DAO_Set';
- }
- if($name == 'price_field' || $name == 'PriceField'){
- return 'CRM_Price_DAO_Field';
- }
- if($name == 'price_field_value' || $name == 'PriceFieldValue'){
- 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';
}
}
/**
- * 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
$options['return']['id'] = TRUE;// ensure 'id' is included
$allfields = _civicrm_api3_get_unique_name_array($dao);
$returnMatched = array_intersect(array_keys($options['return']), $allfields);
- $returnUniqueMatched = array_intersect(
- array_diff(// not already matched on the field names
- $options['return'],
- $returnMatched),
- array_flip($allfields)// but a match for the field keys
- );
-
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]);
-
}
}
}
$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
);
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();
}
}
+/**
+ * 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
+ * @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
}
$fields = civicrm_api($entity, 'getfields', array('version' => 3, 'action' => $action));
$fields = array_intersect_key($fields['values'], $params);
- foreach ($fields as $fieldname => $fieldInfo) {
+ 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])) {
+ 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);
}
}
}
* 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 string $fieldName uniquename of field being checked
* @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
}
$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]);
}
}
* Validate foreign constraint fields being passed into API.
*
* @param array $params params from civicrm_api
- * @param string $fieldname uniquename of field being checked
+ * @param string $fieldName uniquename of field being checked
* @param array $fieldinfo array of fields from getfields function
*/
-function _civicrm_api3_validate_constraint(&$params, &$fieldname, &$fieldInfo) {
+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]);
}
}
* Validate foreign constraint fields being passed into API.
*
* @param array $params params from civicrm_api
- * @param string $fieldname uniquename of field being checked
+ * @param string $fieldName uniquename of field being checked
* @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']);
}
}
}
foreach ($customfields as $key => $value) {
+ // Regular fields have a 'name' property
+ $value['name'] = 'custom_' . $key;
$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;
* 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 string $fieldName uniquename of field being checked
* @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 (!empty($fieldInfo['options'])) {
- $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;
+}
+
+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"));
}
}
}
/**
* Validate string fields being passed into API.
* @param array $params params from civicrm_api
- * @param string $fieldname uniquename of field being checked
+ * @param string $fieldName uniquename of field being checked
* @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 = CRM_Utils_Array::value($fieldname, $params, '');
+ $value = CRM_Utils_Array::value($fieldName, $params, '');
if(!is_array($value)){
$value = (string) $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 (!is_array($value) && !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'])) {
+ _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) {
+ $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
+ $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)
+ */
+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;
+}