utils.php 80.9 KB
Newer Older
totten's avatar
totten committed
1 2
<?php
/*
3 4 5 6 7 8 9
 +--------------------------------------------------------------------+
 | Copyright CiviCRM LLC. All rights reserved.                        |
 |                                                                    |
 | This work is published under the GNU AGPLv3 license with some      |
 | permitted exceptions and without any warranty. For full license    |
 | and copyright information, see https://civicrm.org/licensing       |
 +--------------------------------------------------------------------+
10
 */
totten's avatar
totten committed
11 12

/**
colemanw's avatar
colemanw committed
13
 * CiviCRM APIv3 utility functions.
totten's avatar
totten committed
14 15 16 17 18
 *
 * @package CiviCRM_APIv3
 */

/**
Eileen McNaughton's avatar
Eileen McNaughton committed
19
 * Initialize CiviCRM - should be run at the start of each API function.
totten's avatar
totten committed
20 21
 */
function _civicrm_api3_initialize() {
22 23 24 25
  require_once 'CRM/Core/ClassLoader.php';
  CRM_Core_ClassLoader::singleton()->register();
  CRM_Core_Config::singleton();
}
totten's avatar
totten committed
26

totten's avatar
totten committed
27
/**
Eileen McNaughton's avatar
Eileen McNaughton committed
28
 * Wrapper Function for civicrm_verify_mandatory to make it simple to pass either / or fields for checking.
totten's avatar
totten committed
29
 *
30 31 32 33 34 35
 * @param array $params
 *   Array of fields to check.
 * @param array $daoName
 *   String DAO to check for required fields (create functions only).
 * @param array $keyoptions
 *   List of required fields options. One of the options is required.
36 37
 *
 * @throws \API_Exception
totten's avatar
totten committed
38
 */
39 40
function civicrm_api3_verify_one_mandatory($params, $daoName = NULL, $keyoptions = []) {
  $keys = [[]];
totten's avatar
totten committed
41 42 43 44 45 46
  foreach ($keyoptions as $key) {
    $keys[0][] = $key;
  }
  civicrm_api3_verify_mandatory($params, $daoName, $keys);
}

totten's avatar
totten committed
47
/**
Eileen McNaughton's avatar
Eileen McNaughton committed
48
 * Check mandatory fields are included.
totten's avatar
totten committed
49
 *
50 51 52 53 54 55
 * @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. A value can be an array denoting that either this or that is required.
totten's avatar
totten committed
56
 * @param bool $verifyDAO
Eileen McNaughton's avatar
Eileen McNaughton committed
57 58
 *
 * @throws \API_Exception
totten's avatar
totten committed
59
 */
60 61
function civicrm_api3_verify_mandatory($params, $daoName = NULL, $keys = [], $verifyDAO = TRUE) {
  $unmatched = [];
totten's avatar
totten committed
62 63

  if (!empty($params['id'])) {
64
    $keys = ['version'];
totten's avatar
totten committed
65 66 67 68 69 70 71 72 73 74
  }
  else {
    if (!in_array('version', $keys)) {
      // required from v3 onwards
      $keys[] = 'version';
    }
  }
  foreach ($keys as $key) {
    if (is_array($key)) {
      $match = 0;
75
      $optionset = [];
totten's avatar
totten committed
76 77 78 79 80
      foreach ($key as $subkey) {
        if (!array_key_exists($subkey, $params) || empty($params[$subkey])) {
          $optionset[] = $subkey;
        }
        else {
xurizaemon's avatar
xurizaemon committed
81
          // As long as there is one match we don't need to return anything.
totten's avatar
totten committed
82 83 84 85 86 87 88 89
          $match = 1;
        }
      }
      if (empty($match) && !empty($optionset)) {
        $unmatched[] = "one of (" . implode(", ", $optionset) . ")";
      }
    }
    else {
90
      // Disallow empty values except for the number zero.
xurizaemon's avatar
xurizaemon committed
91
      // TODO: create a utility for this since it's needed in many places.
92
      if (!array_key_exists($key, $params) || (empty($params[$key]) && $params[$key] !== 0 && $params[$key] !== '0')) {
totten's avatar
totten committed
93 94 95 96 97
        $unmatched[] = $key;
      }
    }
  }
  if (!empty($unmatched)) {
98
    throw new API_Exception('Mandatory key(s) missing from params array: ' . implode(", ", $unmatched), 'mandatory_missing', ["fields" => $unmatched]);
totten's avatar
totten committed
99 100 101 102
  }
}

/**
Eileen McNaughton's avatar
Eileen McNaughton committed
103
 * Create error array.
totten's avatar
totten committed
104
 *
Eileen McNaughton's avatar
Eileen McNaughton committed
105
 * @param string $msg
Vagrant User's avatar
Vagrant User committed
106
 * @param array $data
Eileen McNaughton's avatar
Eileen McNaughton committed
107
 *
108
 * @return array
totten's avatar
totten committed
109
 */
110
function civicrm_api3_create_error($msg, $data = []) {
totten's avatar
totten committed
111 112
  $data['is_error'] = 1;
  $data['error_message'] = $msg;
113

totten's avatar
totten committed
114 115
  // we will show sql to privileged user only (not sure of a specific
  // security hole here but seems sensible - perhaps should apply to the trace as well?)
116
  if (isset($data['sql'])) {
117
    if (CRM_Core_Permission::check('Administer CiviCRM') || CIVICRM_UF === 'UnitTests') {
118 119 120 121 122 123
      // Isn't this redundant?
      $data['debug_information'] = $data['sql'];
    }
    else {
      unset($data['sql']);
    }
124
  }
totten's avatar
totten committed
125 126 127 128
  return $data;
}

/**
Eileen McNaughton's avatar
Eileen McNaughton committed
129
 * Format array in result output style.
totten's avatar
totten committed
130
 *
131
 * @param array|int $values values generated by API operation (the result)
132 133 134 135 136 137 138 139 140 141
 * @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.
 * @param object $dao
 *   DAO object to be freed here.
 * @param array $extraReturnValues
 *   Additional values to be added to top level of result array(.
totten's avatar
totten committed
142 143
 *   - this param is currently used for legacy behaviour support
 *
144
 * @return array
145
 * @throws \CiviCRM_API3_Exception
totten's avatar
totten committed
146
 */
147 148
function civicrm_api3_create_success($values = 1, $params = [], $entity = NULL, $action = NULL, &$dao = NULL, $extraReturnValues = []) {
  $result = [];
149 150 151
  $lowercase_entity = _civicrm_api_get_entity_name_from_camel($entity);
  // TODO: This shouldn't be necessary but this fn sometimes gets called with lowercase entity
  $entity = _civicrm_api_get_camel_name($entity);
totten's avatar
totten committed
152 153
  $result['is_error'] = 0;
  //lets set the ['id'] field if it's not set & we know what the entity is
154
  if (is_array($values) && $entity && $action !== 'getfields') {
totten's avatar
totten committed
155
    foreach ($values as $key => $item) {
156 157
      if (empty($item['id']) && !empty($item[$lowercase_entity . '_id'])) {
        $values[$key]['id'] = $item[$lowercase_entity . '_id'];
totten's avatar
totten committed
158
      }
totten's avatar
totten committed
159
      if (!empty($item['financial_type_id'])) {
Seamus Lee's avatar
Seamus Lee committed
160 161
        // 4.3 legacy handling.
        $values[$key]['contribution_type_id'] = $item['financial_type_id'];
162
      }
163 164 165 166
      if (!empty($item['contribution_cancel_date'])) {
        // 5.16 legacy handling.
        $values[$key]['cancel_date'] = $item['contribution_cancel_date'];
      }
totten's avatar
totten committed
167
      if (!empty($item['next_sched_contribution_date'])) {
168 169 170
        // 4.4 legacy handling
        $values[$key]['next_sched_contribution'] = $item['next_sched_contribution_date'];
      }
totten's avatar
totten committed
171 172
    }
  }
eileen's avatar
eileen committed
173

colemanw's avatar
colemanw committed
174
  if (is_array($params) && $entity && !empty($params['debug'])) {
175
    if (is_string($action) && $action !== 'getfields') {
176
      $apiFields = civicrm_api($entity, 'getfields', ['version' => 3, 'action' => $action] + $params);
totten's avatar
totten committed
177
    }
178
    elseif ($action !== 'getfields') {
179
      $apiFields = civicrm_api($entity, 'getfields', ['version' => 3] + $params);
totten's avatar
totten committed
180 181 182 183 184
    }
    else {
      $apiFields = FALSE;
    }

185
    $allFields = [];
186
    if ($action !== 'getfields' && isset($apiFields['values']) && is_array($apiFields['values'])) {
totten's avatar
totten committed
187 188 189
      $allFields = array_keys($apiFields['values']);
    }
    $paramFields = array_keys($params);
190
    $undefined = array_diff($paramFields, $allFields, array_keys($_COOKIE), [
Eileen McNaughton's avatar
Eileen McNaughton committed
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
      'action',
      'entity',
      'debug',
      'version',
      'check_permissions',
      'IDS_request_uri',
      'IDS_user_agent',
      'return',
      'sequential',
      'rowCount',
      'option_offset',
      'option_limit',
      'custom',
      'option_sort',
      'options',
      'prettyprint',
207
    ]);
totten's avatar
totten committed
208 209 210 211 212 213 214
    if ($undefined) {
      $result['undefined_fields'] = array_merge($undefined);
    }
  }

  $result['version'] = 3;
  if (is_array($values)) {
215
    $result['count'] = (int) count($values);
totten's avatar
totten committed
216 217

    // Convert value-separated strings to array
218
    if ($action !== 'getfields') {
219 220
      _civicrm_api3_separate_values($values);
    }
totten's avatar
totten committed
221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240

    if ($result['count'] == 1) {
      list($result['id']) = array_keys($values);
    }
    elseif (!empty($values['id']) && is_int($values['id'])) {
      $result['id'] = $values['id'];
    }
  }
  else {
    $result['count'] = !empty($values) ? 1 : 0;
  }

  if (is_array($values) && isset($params['sequential']) &&
    $params['sequential'] == 1
  ) {
    $result['values'] = array_values($values);
  }
  else {
    $result['values'] = $values;
  }
241
  if (!empty($params['options']['metadata'])) {
Eileen McNaughton's avatar
Eileen McNaughton committed
242
    // We've made metadata an array but only supporting 'fields' atm.
243
    if (in_array('fields', (array) $params['options']['metadata']) && $action !== 'getfields') {
244
      $fields = civicrm_api3($entity, 'getfields', [
Eileen McNaughton's avatar
Eileen McNaughton committed
245
        'action' => substr($action, 0, 3) == 'get' ? 'get' : 'create',
246
      ]);
247 248 249
      $result['metadata']['fields'] = $fields['values'];
    }
  }
Eileen McNaughton's avatar
Eileen McNaughton committed
250
  // Report deprecations.
251
  $deprecated = _civicrm_api3_deprecation_check($entity, $result);
252
  // Always report "setvalue" action as deprecated.
253
  if (!is_string($deprecated) && ($action === 'getactions' || $action === 'setvalue')) {
254
    $deprecated = ((array) $deprecated) + ['setvalue' => 'The "setvalue" action is deprecated. Use "create" with an id instead.'];
255
  }
Eileen McNaughton's avatar
Eileen McNaughton committed
256
  // Always report "update" action as deprecated.
257
  if (!is_string($deprecated) && ($action === 'getactions' || $action === 'update')) {
258
    $deprecated = ((array) $deprecated) + ['update' => 'The "update" action is deprecated. Use "create" with an id instead.'];
259 260
  }
  if ($deprecated) {
Eileen McNaughton's avatar
Eileen McNaughton committed
261
    // Metadata-level deprecations or wholesale entity deprecations.
262
    if ($entity === 'Entity' || $action === 'getactions' || is_string($deprecated)) {
263 264 265 266 267 268 269
      $result['deprecated'] = $deprecated;
    }
    // Action-specific deprecations
    elseif (!empty($deprecated[$action])) {
      $result['deprecated'] = $deprecated[$action];
    }
  }
totten's avatar
totten committed
270 271
  return array_merge($result, $extraReturnValues);
}
totten's avatar
totten committed
272 273

/**
Eileen McNaughton's avatar
Eileen McNaughton committed
274 275
 * Load the DAO of the entity.
 *
Eileen McNaughton's avatar
Eileen McNaughton committed
276
 * @param $entity
Eileen McNaughton's avatar
Eileen McNaughton committed
277
 *
Eileen McNaughton's avatar
Eileen McNaughton committed
278
 * @return bool
totten's avatar
totten committed
279 280 281 282 283 284 285 286 287
 */
function _civicrm_api3_load_DAO($entity) {
  $dao = _civicrm_api3_get_DAO($entity);
  if (empty($dao)) {
    return FALSE;
  }
  $d = new $dao();
  return $d;
}
totten's avatar
totten committed
288 289

/**
Eileen McNaughton's avatar
Eileen McNaughton committed
290 291
 * Return the DAO of the function or Entity.
 *
292 293
 * @param string $name
 *   Either a function of the api (civicrm_{entity}_create or the entity name.
294 295
 *   return the DAO name to manipulate this function
 *   eg. "civicrm_api3_contact_create" or "Contact" will return "CRM_Contact_BAO_Contact"
Eileen McNaughton's avatar
Eileen McNaughton committed
296
 *
Eileen's avatar
Eileen committed
297
 * @return mixed|string
totten's avatar
totten committed
298 299 300 301 302 303 304 305
 */
function _civicrm_api3_get_DAO($name) {
  if (strpos($name, 'civicrm_api3') !== FALSE) {
    $last = strrpos($name, '_');
    // len ('civicrm_api3_') == 13
    $name = substr($name, 13, $last - 13);
  }

306
  $name = _civicrm_api_get_camel_name($name);
totten's avatar
totten committed
307

308
  if ($name === 'Individual' || $name === 'Household' || $name === 'Organization') {
totten's avatar
totten committed
309 310 311
    $name = 'Contact';
  }

312 313
  // hack to deal with incorrectly named BAO/DAO - see CRM-10859

314
  // FIXME: DAO should be renamed CRM_Mailing_DAO_MailingEventQueue
315
  if ($name === 'MailingEventQueue') {
316 317
    return 'CRM_Mailing_Event_DAO_Queue';
  }
318 319
  // FIXME: DAO should be renamed CRM_Mailing_DAO_MailingRecipients
  // but am not confident mailing_recipients is tested so have not tackled.
320
  if ($name === 'MailingRecipients') {
321
    return 'CRM_Mailing_DAO_Recipients';
totten's avatar
totten committed
322
  }
323
  // FIXME: DAO should be renamed CRM_ACL_DAO_AclRole
324
  if ($name === 'AclRole') {
325 326
    return 'CRM_ACL_DAO_EntityRole';
  }
327 328 329
  // FIXME: DAO should be renamed CRM_SMS_DAO_SmsProvider
  // But this would impact SMS extensions so need to coordinate
  // Probably best approach is to migrate them to use the api and decouple them from core BAOs
330
  if ($name === 'SmsProvider') {
331 332 333
    return 'CRM_SMS_DAO_Provider';
  }
  // FIXME: DAO names should follow CamelCase convention
334
  if ($name === 'Im' || $name === 'Acl' || $name === 'Pcp') {
335
    $name = strtoupper($name);
totten's avatar
totten committed
336
  }
337
  $dao = CRM_Core_DAO_AllCoreTables::getFullName($name);
colemanw's avatar
colemanw committed
338
  if ($dao || !$name) {
339 340 341 342
    return $dao;
  }

  // Really weird apis can declare their own DAO name. Not sure if this is a good idea...
343
  if (file_exists("api/v3/$name.php")) {
jitendrapurohit's avatar
jitendrapurohit committed
344 345
    include_once "api/v3/$name.php";
  }
346

347
  $daoFn = '_civicrm_api3_' . _civicrm_api_get_entity_name_from_camel($name) . '_DAO';
348 349 350 351 352
  if (function_exists($daoFn)) {
    return $daoFn();
  }

  return NULL;
totten's avatar
totten committed
353 354
}

totten's avatar
totten committed
355
/**
356
 * Return the BAO name of the function or Entity.
Eileen McNaughton's avatar
Eileen McNaughton committed
357
 *
358 359
 * @param string $name
 *   Is either a function of the api (civicrm_{entity}_create or the entity name.
360 361
 *   return the DAO name to manipulate this function
 *   eg. "civicrm_contact_create" or "Contact" will return "CRM_Contact_BAO_Contact"
Eileen McNaughton's avatar
Eileen McNaughton committed
362
 *
colemanw's avatar
colemanw committed
363
 * @return string|null
totten's avatar
totten committed
364 365
 */
function _civicrm_api3_get_BAO($name) {
366
  // FIXME: DAO should be renamed CRM_Badge_DAO_BadgeLayout
367
  if ($name === 'PrintLabel') {
368 369
    return 'CRM_Badge_BAO_Layout';
  }
370 371 372 373 374
  if ($name === 'Order') {
    // Order basically maps to contribution at the top level but
    // has enhanced access to other entities.
    $name = 'Contribution';
  }
375 376 377 378 379
  if ($name === 'Dedupe') {
    // Dedupe is a pseudoentity for PrevNextCache - but accessing dedupe related info
    // not the other cache info like search results (which could in fact be in Redis or another cache engine)
    $name = 'PrevNextCache';
  }
380 381 382
  if ($name === 'Payment') {
    $name = 'FinancialTrxn';
  }
totten's avatar
totten committed
383
  $dao = _civicrm_api3_get_DAO($name);
384 385 386
  if (!$dao) {
    return NULL;
  }
387
  $bao = str_replace("DAO", "BAO", $dao);
388
  $file = strtr($bao, '_', '/') . '.php';
389
  // Check if this entity actually has a BAO. Fall back on the DAO if not.
390
  return stream_resolve_include_path($file) ? $bao : $dao;
totten's avatar
totten committed
391 392 393
}

/**
Eileen McNaughton's avatar
Eileen McNaughton committed
394 395
 * Recursive function to explode value-separated strings into arrays.
 *
Eileen McNaughton's avatar
Eileen McNaughton committed
396
 * @param $values
totten's avatar
totten committed
397 398 399 400 401 402 403 404
 */
function _civicrm_api3_separate_values(&$values) {
  $sp = CRM_Core_DAO::VALUE_SEPARATOR;
  foreach ($values as $key => & $value) {
    if (is_array($value)) {
      _civicrm_api3_separate_values($value);
    }
    elseif (is_string($value)) {
Eileen McNaughton's avatar
Eileen McNaughton committed
405
      // This is to honor the way case API was originally written.
406
      if ($key === 'case_type_id') {
totten's avatar
totten committed
407 408 409 410 411 412 413 414
        $value = trim(str_replace($sp, ',', $value), ',');
      }
      elseif (strpos($value, $sp) !== FALSE) {
        $value = explode($sp, trim($value, $sp));
      }
    }
  }
}
totten's avatar
totten committed
415 416

/**
Eileen McNaughton's avatar
Eileen McNaughton committed
417 418 419
 * This is a legacy wrapper for api_store_values.
 *
 * It checks suitable fields using getfields rather than DAO->fields.
totten's avatar
totten committed
420
 *
Eileen McNaughton's avatar
Eileen McNaughton committed
421
 * Getfields has handling for how to deal with unique names which dao->fields doesn't
totten's avatar
totten committed
422 423
 *
 * Note this is used by BAO type create functions - eg. contribution
Eileen McNaughton's avatar
Eileen McNaughton committed
424
 *
totten's avatar
totten committed
425 426 427 428
 * @param string $entity
 * @param array $params
 * @param array $values
 */
totten's avatar
totten committed
429
function _civicrm_api3_filter_fields_for_bao($entity, &$params, &$values) {
430
  $fields = civicrm_api($entity, 'getfields', ['version' => 3, 'action' => 'create']);
totten's avatar
totten committed
431 432 433
  $fields = $fields['values'];
  _civicrm_api3_store_values($fields, $params, $values);
}
434

totten's avatar
totten committed
435
/**
Eileen McNaughton's avatar
Eileen McNaughton committed
436
 * Store values.
totten's avatar
totten committed
437 438 439 440 441
 *
 * @param array $fields
 * @param array $params
 * @param array $values
 *
442
 * @return Bool
totten's avatar
totten committed
443 444 445 446 447 448 449 450 451 452 453 454 455
 */
function _civicrm_api3_store_values(&$fields, &$params, &$values) {
  $valueFound = FALSE;

  $keys = array_intersect_key($params, $fields);
  foreach ($keys as $name => $value) {
    if ($name !== 'id') {
      $values[$name] = $value;
      $valueFound = TRUE;
    }
  }
  return $valueFound;
}
Eileen's avatar
Eileen committed
456

457 458 459
/**
 * Returns field names of the given entity fields.
 *
460
 * @deprecated
461
 * @param array $fields
462 463 464 465
 *   Fields array to retrieve the field names for.
 * @return array
 */
function _civicrm_api3_field_names($fields) {
466
  CRM_Core_Error::deprecatedFunctionWarning('array_column');
467
  $result = [];
Johan Vervloet's avatar
Johan Vervloet committed
468
  foreach ($fields as $key => $value) {
469
    if (!empty($value['name'])) {
Johan Vervloet's avatar
Johan Vervloet committed
470
      $result[] = $value['name'];
471
    }
472 473 474 475
  }
  return $result;
}

totten's avatar
totten committed
476
/**
Eileen McNaughton's avatar
Eileen McNaughton committed
477 478 479
 * Get function for query object api.
 *
 * The API supports 2 types of get request. The more complex uses the BAO query object.
totten's avatar
totten committed
480 481 482 483 484 485
 *  This is a generic function for those functions that call it
 *
 *  At the moment only called by contact we should extend to contribution &
 *  others that use the query object. Note that this function passes permission information in.
 *  The others don't
 *
Eileen McNaughton's avatar
Eileen McNaughton committed
486
 * Ideally this would be merged with _civicrm_get_query_object but we need to resolve differences in what the
487
 * 2 variants call
Eileen McNaughton's avatar
Eileen McNaughton committed
488
 *
Eileen's avatar
Eileen committed
489
 * @param $entity
490 491 492 493 494 495
 * @param array $params
 *   As passed into api get or getcount function.
 * @param array $additional_options
 *   Array of options (so we can modify the filter).
 * @param bool $getCount
 *   Are we just after the count.
496 497
 * @param int $mode
 *   This basically correlates to the component.
eileen's avatar
eileen committed
498 499 500
 * @param null|array $defaultReturnProperties
 *   Default return properties for the entity
 *  (used if return not set - but don't do that - set return!).
Eileen's avatar
Eileen committed
501
 *
totten's avatar
totten committed
502
 * @return array
eileen's avatar
eileen committed
503
 * @throws API_Exception
totten's avatar
totten committed
504
 */
505
function _civicrm_api3_get_using_query_object($entity, $params, $additional_options = [], $getCount = NULL, $mode = 1, $defaultReturnProperties = NULL) {
506
  $lowercase_entity = _civicrm_api_get_entity_name_from_camel($entity);
totten's avatar
totten committed
507
  // Convert id to e.g. contact_id
508 509
  if (empty($params[$lowercase_entity . '_id']) && isset($params['id'])) {
    $params[$lowercase_entity . '_id'] = $params['id'];
totten's avatar
totten committed
510 511 512 513 514 515
  }
  unset($params['id']);

  $options = _civicrm_api3_get_options_from_params($params, TRUE);

  $inputParams = array_merge(
516 517
    CRM_Utils_Array::value('input_params', $options, []),
    CRM_Utils_Array::value('input_params', $additional_options, [])
totten's avatar
totten committed
518 519
  );
  $returnProperties = array_merge(
520 521
    CRM_Utils_Array::value('return', $options, []),
    CRM_Utils_Array::value('return', $additional_options, [])
totten's avatar
totten committed
522
  );
totten's avatar
totten committed
523
  if (empty($returnProperties)) {
524
    $returnProperties = $defaultReturnProperties;
totten's avatar
totten committed
525
  }
totten's avatar
totten committed
526
  if (!empty($params['check_permissions'])) {
totten's avatar
totten committed
527
    // we will filter query object against getfields
528
    $fields = civicrm_api($entity, 'getfields', ['version' => 3, 'action' => 'get']);
totten's avatar
totten committed
529
    // we need to add this in as earlier in this function 'id' was unset in favour of $entity_id
530 531
    $fields['values'][$lowercase_entity . '_id'] = [];
    $varsToFilter = ['returnProperties', 'inputParams'];
totten's avatar
totten committed
532 533
    foreach ($varsToFilter as $varToFilter) {
      if (!is_array($$varToFilter)) {
totten's avatar
totten committed
534 535 536 537 538 539 540 541 542
        continue;
      }
      //I was going to throw an exception rather than silently filter out - but
      //would need to diff out of exceptions arr other keys like 'options', 'return', 'api. etcetc
      //so we are silently ignoring parts of their request
      //$exceptionsArr = array_diff(array_keys($$varToFilter), array_keys($fields['values']));
      $$varToFilter = array_intersect_key($$varToFilter, $fields['values']);
    }
  }
totten's avatar
totten committed
543
  $options = array_merge($options, $additional_options);
544 545 546 547
  $sort             = $options['sort'] ?? NULL;
  $offset             = $options['offset'] ?? NULL;
  $limit             = $options['limit'] ?? NULL;
  $smartGroupCache  = $params['smartGroupCache'] ?? NULL;
totten's avatar
totten committed
548

totten's avatar
totten committed
549
  if ($getCount) {
totten's avatar
totten committed
550 551 552 553
    $limit = NULL;
    $returnProperties = NULL;
  }

554
  if (substr($sort, 0, 2) == 'id') {
555
    $sort = $lowercase_entity . "_" . $sort;
556 557
  }

totten's avatar
totten committed
558
  $newParams = CRM_Contact_BAO_Query::convertFormValues($inputParams);
559

totten's avatar
totten committed
560
  $skipPermissions = !empty($params['check_permissions']) ? 0 : 1;
561

562
  list($entities) = CRM_Contact_BAO_Query::apiQuery(
totten's avatar
totten committed
563 564 565 566
    $newParams,
    $returnProperties,
    NULL,
    $sort,
totten's avatar
totten committed
567
    $offset,
totten's avatar
totten committed
568 569 570
    $limit,
    $smartGroupCache,
    $getCount,
571
    $skipPermissions,
572
    $mode,
573 574
    $entity,
    TRUE
totten's avatar
totten committed
575 576 577 578
  );

  return $entities;
}
totten's avatar
totten committed
579

580
/**
Eileen McNaughton's avatar
Eileen McNaughton committed
581 582
 * Get dao query object based on input params.
 *
583 584 585 586 587 588
 * Ideally this would be merged with _civicrm_get_using_query_object but we need to resolve differences in what the
 * 2 variants call
 *
 * @param array $params
 * @param string $mode
 * @param string $entity
Eileen McNaughton's avatar
Eileen McNaughton committed
589
 *
colemanw's avatar
colemanw committed
590 591
 * @return array
 *   [CRM_Core_DAO|CRM_Contact_BAO_Query]
592 593
 */
function _civicrm_api3_get_query_object($params, $mode, $entity) {
colemanw's avatar
colemanw committed
594
  $options = _civicrm_api3_get_options_from_params($params, TRUE, $entity, 'get');
595 596 597
  $sort = $options['sort'] ?? NULL;
  $offset = $options['offset'] ?? NULL;
  $rowCount = $options['limit'] ?? NULL;
598
  $inputParams = CRM_Utils_Array::value('input_params', $options, []);
599
  $returnProperties = $options['return'] ?? NULL;
600 601 602 603
  if (empty($returnProperties)) {
    $returnProperties = CRM_Contribute_BAO_Query::defaultReturnProperties($mode);
  }

monishdeb's avatar
monishdeb committed
604
  $newParams = CRM_Contact_BAO_Query::convertFormValues($inputParams, 0, FALSE, $entity);
605
  $query = new CRM_Contact_BAO_Query($newParams, $returnProperties, NULL,
606
    FALSE, FALSE, $mode,
607 608
    empty($params['check_permissions']),
    TRUE, TRUE, NULL, 'AND', 'NULL', TRUE
609 610 611 612 613 614
  );
  list($select, $from, $where, $having) = $query->query();

  $sql = "$select $from $where $having";

  if (!empty($sort)) {
615
    $sort = CRM_Utils_Type::escape($sort, 'MysqlOrderBy');
616 617
    $sql .= " ORDER BY $sort ";
  }
618
  if (!empty($rowCount)) {
619 620 621
    $sql .= " LIMIT $offset, $rowCount ";
  }
  $dao = CRM_Core_DAO::executeQuery($sql);
622
  return [$dao, $query];
623 624
}

totten's avatar
totten committed
625
/**
Eileen McNaughton's avatar
Eileen McNaughton committed
626 627
 * Function transfers the filters being passed into the DAO onto the params object.
 *
628 629
 * @deprecated DAO based retrieval is being phased out.
 *
630 631 632
 * @param CRM_Core_DAO $dao
 * @param array $params
 * @param bool $unique
eileenmcnaugton's avatar
eileenmcnaugton committed
633 634 635
 * @param array $extraSql
 *   API specific queries eg for event isCurrent would be converted to
 *   $extraSql['where'] = array('civicrm_event' => array('(start_date >= CURDATE() || end_date >= CURDATE())'));
636 637 638
 *
 * @throws API_Exception
 * @throws Exception
totten's avatar
totten committed
639
 */
640
function _civicrm_api3_dao_set_filter(&$dao, $params, $unique = TRUE, $extraSql = []) {
641 642 643
  $entity = _civicrm_api_get_entity_name_from_dao($dao);
  $lowercase_entity = _civicrm_api_get_entity_name_from_camel($entity);
  if (!empty($params[$lowercase_entity . "_id"]) && empty($params['id'])) {
totten's avatar
totten committed
644
    //if entity_id is set then treat it as ID (will be overridden by id if set)
645
    $params['id'] = $params[$lowercase_entity . "_id"];
totten's avatar
totten committed
646
  }
647 648
  $allfields = _civicrm_api3_build_fields_array($dao, $unique);
  $fields = array_intersect(array_keys($allfields), array_keys($params));
649 650

  $options = _civicrm_api3_get_options_from_params($params);
totten's avatar
totten committed
651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670
  //apply options like sort
  _civicrm_api3_apply_options_to_dao($params, $dao, $entity);

  //accept filters like filter.activity_date_time_high
  // std is now 'filters' => ..
  if (strstr(implode(',', array_keys($params)), 'filter')) {
    if (isset($params['filters']) && is_array($params['filters'])) {
      foreach ($params['filters'] as $paramkey => $paramvalue) {
        _civicrm_api3_apply_filters_to_dao($paramkey, $paramvalue, $dao);
      }
    }
    else {
      foreach ($params as $paramkey => $paramvalue) {
        if (strstr($paramkey, 'filter')) {
          _civicrm_api3_apply_filters_to_dao(substr($paramkey, 7), $paramvalue, $dao);
        }
      }
    }
  }
  if (!$fields) {
671
    $fields = [];
totten's avatar
totten committed
672 673 674 675 676 677
  }

  foreach ($fields as $field) {
    if (is_array($params[$field])) {
      //get the actual fieldname from db
      $fieldName = $allfields[$field]['name'];
678
      $where = CRM_Core_DAO::createSqlFilter($fieldName, $params[$field], 'String');
679
      if (!empty($where)) {
680
        $dao->whereAdd($where);
totten's avatar
totten committed
681 682 683 684
      }
    }
    else {
      if ($unique) {
685 686 687 688 689
        $daoFieldName = $allfields[$field]['name'];
        if (empty($daoFieldName)) {
          throw new API_Exception("Failed to determine field name for \"$field\"");
        }
        $dao->{$daoFieldName} = $params[$field];
totten's avatar
totten committed
690 691 692 693 694 695
      }
      else {
        $dao->$field = $params[$field];
      }
    }
  }
eileenmcnaugton's avatar
eileenmcnaugton committed
696 697 698 699 700 701 702
  if (!empty($extraSql['where'])) {
    foreach ($extraSql['where'] as $table => $sqlWhere) {
      foreach ($sqlWhere as $where) {
        $dao->whereAdd($where);
      }
    }
  }
703
  if (!empty($options['return']) && is_array($options['return']) && empty($options['is_count'])) {
totten's avatar
totten committed
704
    $dao->selectAdd();
Eileen McNaughton's avatar
Eileen McNaughton committed
705 706
    // Ensure 'id' is included.
    $options['return']['id'] = TRUE;
totten's avatar
totten committed
707
    $allfields = _civicrm_api3_get_unique_name_array($dao);
708
    $returnMatched = array_intersect(array_keys($options['return']), $allfields);
totten's avatar
totten committed
709
    foreach ($returnMatched as $returnValue) {
710
      $dao->selectAdd($returnValue);
totten's avatar
totten committed
711
    }
712

Eileen McNaughton's avatar
Eileen McNaughton committed
713 714
    // Not already matched on the field names.
    $unmatchedFields = array_diff(
715 716 717 718 719 720
      array_keys($options['return']),
      $returnMatched
    );

    $returnUniqueMatched = array_intersect(
      $unmatchedFields,
Eileen McNaughton's avatar
Eileen McNaughton committed
721 722
      // But a match for the field keys.
      array_flip($allfields)
723
    );
totten's avatar
totten committed
724
    foreach ($returnUniqueMatched as $uniqueVal) {
totten's avatar
totten committed
725 726 727
      $dao->selectAdd($allfields[$uniqueVal]);
    }
  }
728
  $dao->setApiFilter($params);
totten's avatar
totten committed
729 730
}

totten's avatar
totten committed
731
/**
Eileen McNaughton's avatar
Eileen McNaughton committed
732 733
 * Apply filters (e.g. high, low) to DAO object (prior to find).
 *
734 735 736 737 738 739
 * @param string $filterField
 *   Field name of filter.
 * @param string $filterValue
 *   Field value of filter.
 * @param object $dao
 *   DAO object.
totten's avatar
totten committed
740 741 742 743 744 745 746 747 748 749
 */
function _civicrm_api3_apply_filters_to_dao($filterField, $filterValue, &$dao) {
  if (strstr($filterField, 'high')) {
    $fieldName = substr($filterField, 0, -5);
    $dao->whereAdd("($fieldName <= $filterValue )");
  }
  if (strstr($filterField, 'low')) {
    $fieldName = substr($filterField, 0, -4);
    $dao->whereAdd("($fieldName >= $filterValue )");
  }
totten's avatar
totten committed
750
  if ($filterField == 'is_current' && $filterValue == 1) {
totten's avatar
totten committed
751 752 753
    $todayStart = date('Ymd000000', strtotime('now'));
    $todayEnd = date('Ymd235959', strtotime('now'));
    $dao->whereAdd("(start_date <= '$todayStart' OR start_date IS NULL) AND (end_date >= '$todayEnd' OR end_date IS NULL)");
totten's avatar
totten committed
754
    if (property_exists($dao, 'is_active')) {
totten's avatar
totten committed
755 756 757 758
      $dao->whereAdd('is_active = 1');
    }
  }
}
totten's avatar
totten committed
759 760

/**
totten's avatar
totten committed
761
 * Get sort, limit etc options from the params - supporting old & new formats.
Eileen McNaughton's avatar
Eileen McNaughton committed
762 763
 *
 * Get returnProperties for legacy
Eileen's avatar
Eileen committed
764
 *
765 766 767
 * @param array $params
 *   Params array as passed into civicrm_api.
 * @param bool $queryObject
Eileen McNaughton's avatar
Eileen McNaughton committed
768
 *   Is this supporting a queryObject api (e.g contact) - if so we support more options.
769
 *   for legacy report & return a unique fields array
Eileen's avatar
Eileen committed
770 771 772 773
 *
 * @param string $entity
 * @param string $action
 *
Eileen McNaughton's avatar
Eileen McNaughton committed
774
 * @throws API_Exception
775
 * @return array
776
 *   options extracted from params
totten's avatar
totten committed
777
 */
colemanw's avatar
colemanw committed
778
function _civicrm_api3_get_options_from_params($params, $queryObject = FALSE, $entity = '', $action = '') {
779
  $lowercase_entity = _civicrm_api_get_entity_name_from_camel($entity);
780
  $is_count = FALSE;
totten's avatar
totten committed
781 782

  // dear PHP thought it would be a good idea to transform a.b into a_b in the get/post
783 784
  $sort = $params['option_sort'] ?? $params['option.sort'] ?? $params['sort'] ?? 0;
  $offset = $params['option_offset'] ?? $params['option.offset'] ?? $params['offset'] ?? 0;
totten's avatar
totten committed
785 786 787 788 789

  $limit = CRM_Utils_Array::value('rowCount', $params, 25);
  $limit = CRM_Utils_Array::value('option.limit', $params, $limit);
  $limit = CRM_Utils_Array::value('option_limit', $params, $limit);

790
  if (isset($params['options']) && is_array($params['options'])) {
791
    // is count is set by generic getcount not user
792 793 794 795
    $is_count = $params['options']['is_count'] ?? FALSE;
    $offset = $params['options']['offset'] ?? $offset;
    $limit = CRM_Utils_Array::value('limit', $params['options'], $limit);
    $sort = $params['options']['sort'] ?? $sort;
totten's avatar
totten committed
796 797
  }

798
  $returnProperties = [];
totten's avatar
totten committed
799 800 801 802 803 804 805 806 807 808
  // handle the format return =sort_name,display_name...
  if (array_key_exists('return', $params)) {
    if (is_array($params['return'])) {
      $returnProperties = array_fill_keys($params['return'], 1);
    }
    else {
      $returnProperties = explode(',', str_replace(' ', '', $params['return']));
      $returnProperties = array_fill_keys($returnProperties, 1);
    }
  }
totten's avatar
totten committed
809
  if ($entity && $action == 'get') {
810
    if (!empty($returnProperties['id'])) {
811
      $returnProperties[$lowercase_entity . '_id'] = 1;
totten's avatar
totten committed
812 813 814 815
      unset($returnProperties['id']);
    }
  }

816
  $options = [
817
    'offset' => CRM_Utils_Rule::integer($offset) ? $offset : NULL,
818
    'limit' => (!$is_count && CRM_Utils_Rule::integer($limit)) ? $limit : NULL,
819
    'is_count' => $is_count,
820 821
    'return' => !empty($returnProperties) ? $returnProperties : [],
  ];
deb.monish's avatar
deb.monish committed
822

823
  $finalSort = [];
deb.monish's avatar
deb.monish committed
824 825
  $options['sort'] = NULL;
  if (!empty($sort)) {
826 827 828 829
    if (!is_array($sort)) {
      $sort = array_map('trim', explode(',', $sort));
    }
    foreach ($sort as $s) {
830 831
      if ($s === '(1)' || CRM_Utils_Rule::mysqlOrderBy($s)) {
        if ($entity && $action === 'get') {
832 833 834 835 836 837 838
          switch (trim(strtolower($s))) {
            case 'id':
            case 'id desc':
            case 'id asc':
              $s = str_replace('id', $lowercase_entity . '_id', $s);
          }
        }
839 840 841 842 843 844
        $finalSort[] = $s;
      }
      else {
        throw new API_Exception("Unknown field specified for sort. Cannot order by '$s'");
      }
    }
deb.monish's avatar
deb.monish committed
845
    $options['sort'] = implode(', ', $finalSort);
846
  }
847

848
  if ($options['sort'] && stristr($options['sort'], 'SELECT')) {
849 850
    throw new API_Exception('invalid string in sort options');
  }
851

totten's avatar
totten committed
852 853 854 855
  if (!$queryObject) {
    return $options;
  }
  //here comes the legacy support for $returnProperties, $inputParams e.g for contat_get
856
  // if the query object is being used this should be used
857 858 859
  $inputParams = [];
  $legacyreturnProperties = [];
  $otherVars = [
totten's avatar
totten committed
860
    'sort', 'offset', 'rowCount', 'options', 'return',
861
    'version', 'prettyprint', 'check_permissions', 'sequential',
862
  ];
totten's avatar
totten committed
863
  foreach ($params as $n => $v) {
864
    if (substr($n, 0, 7) === 'return.') {
totten's avatar
totten committed
865 866
      $legacyreturnProperties[substr($n, 7)] = $v;
    }
867
    elseif ($n === 'id') {
868
      $inputParams[$lowercase_entity . '_id'] = $v;
totten's avatar
totten committed
869
    }
870
    elseif (!in_array($n, $otherVars)) {
totten's avatar
totten committed
871
      $inputParams[$n] = $v;
872
      if ($v && !is_array($v) && stristr($v, 'SELECT')) {
873 874
        throw new API_Exception('invalid string');
      }
totten's avatar
totten committed
875 876 877 878 879 880
    }
  }
  $options['return'] = array_merge($returnProperties, $legacyreturnProperties);
  $options['input_params'] = $inputParams;
  return $options;
}
totten's avatar
totten committed
881 882

/**
Eileen McNaughton's avatar
Eileen McNaughton committed
883
 * Apply options (e.g. sort, limit, order by) to DAO object (prior to find).
Eileen's avatar
Eileen committed
884
 *
885 886 887 888
 * @param array $params
 *   Params array as passed into civicrm_api.
 * @param object $dao
 *   DAO object.
Eileen's avatar
Eileen committed
889
 * @param $entity
890 891 892
 *
 * @throws \API_Exception
 * @throws \CRM_Core_Exception
totten's avatar
totten committed
893 894 895
 */
function _civicrm_api3_apply_options_to_dao(&$params, &$dao, $entity) {

totten's avatar
totten committed
896
  $options = _civicrm_api3_get_options_from_params($params, FALSE, $entity);
897 898
  if (!$options['is_count']) {
    if (!empty($options['limit'])) {
totten's avatar
totten committed
899
      $dao->limit((int) $options['offset'], (int) $options['limit']);
900
    }
901
    if (!empty($options['sort'])) {
902
      $options['sort'] = CRM_Utils_Type::escape($options['sort'], 'MysqlOrderBy');
903 904
      $dao->orderBy($options['sort']);
    }
totten's avatar
totten committed
905 906 907
  }
}

totten's avatar
totten committed
908
/**
Eileen McNaughton's avatar
Eileen McNaughton committed
909 910 911
 * Build fields array.
 *
 * This is the array of fields as it relates to the given DAO
totten's avatar
totten committed
912
 * returns unique fields as keys by default but if set but can return by DB fields
colemanw's avatar
colemanw committed
913 914
 *
 * @param CRM_Core_DAO $bao
Eileen McNaughton's avatar
Eileen McNaughton committed
915
 * @param bool $unique
colemanw's avatar
colemanw committed
916 917
 *
 * @return array
totten's avatar
totten committed
918 919 920 921
 */
function _civicrm_api3_build_fields_array(&$bao, $unique = TRUE) {
  $fields = $bao->fields();
  if ($unique) {
totten's avatar
totten committed
922
    if (empty($fields['id'])) {
923
      $lowercase_entity = _civicrm_api_get_entity_name_from_camel(_civicrm_api_get_entity_name_from_dao($bao));
924 925 926 927
      if (isset($fields[$lowercase_entity . '_id'])) {
        $fields['id'] = $fields[$lowercase_entity . '_id'];
        unset($fields[$lowercase_entity . '_id']);
      }
totten's avatar
totten committed
928 929 930 931 932 933 934 935 936 937
    }
    return $fields;
  }

  foreach ($fields as $field) {
    $dbFields[$field['name']] = $field;
  }
  return $dbFields;
}

totten's avatar
totten committed
938
/**
Eileen McNaughton's avatar
Eileen McNaughton committed
939 940 941
 * Build fields array.
 *
 * This is the array of fields as it relates to the given DAO
totten's avatar
totten committed
942
 * returns unique fields as keys by default but if set but can return by DB fields
colemanw's avatar
colemanw committed
943
 *
Eileen McNaughton's avatar
Eileen McNaughton committed
944
 * @param CRM_Core_DAO $bao<