From 2ca7273bed4d24245b932f8378b41385db7746e2 Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Wed, 1 Jul 2020 13:25:31 -0400 Subject: [PATCH] Add upgrade-safe DAO::getSupportedFields method Switches api v3 and v4 to use that method so they are upgrade-safe by default. --- CRM/Core/DAO.php | 37 +++++++++++++++++++++++++ Civi/Api4/Service/Spec/SpecGatherer.php | 2 +- api/v3/utils.php | 16 +++++++---- sql/test_data_second_domain.mysql | 2 +- tests/phpunit/CRM/Core/DAOTest.php | 26 +++++++++++++++++ 5 files changed, 75 insertions(+), 8 deletions(-) diff --git a/CRM/Core/DAO.php b/CRM/Core/DAO.php index 42d07846aa..11a990b52d 100644 --- a/CRM/Core/DAO.php +++ b/CRM/Core/DAO.php @@ -530,6 +530,43 @@ class CRM_Core_DAO extends DB_DataObject { return $result; } + /** + * Returns all usable fields, indexed by name. + * + * This function differs from fields() in that it indexes by name rather than unique_name. + * + * It excludes fields not added yet by pending upgrades. + * This avoids problems with trying to SELECT a field that exists in code but has not yet been added to the db. + * + * @param bool $checkPermissions + * Filter by field permissions. + * @return array + */ + public static function getSupportedFields($checkPermissions = FALSE) { + $fields = array_column((array) static::fields(), NULL, 'name'); + + // Exclude fields yet not added by pending upgrades + $dbVer = \CRM_Core_BAO_Domain::version(); + if ($fields && version_compare($dbVer, \CRM_Utils_System::version()) < 0) { + $fields = array_filter($fields, function($field) use ($dbVer) { + $add = $field['add'] ?? '1.0.0'; + if (substr_count($add, '.') < 2) { + $add .= '.alpha1'; + } + return version_compare($dbVer, $add, '>='); + }); + } + + // Exclude fields the user does not have permission for + if ($checkPermissions) { + $fields = array_filter($fields, function($field) { + return empty($field['permission']) || CRM_Core_Permission::check($field['permission']); + }); + } + + return $fields; + } + /** * Get/set an associative array of table columns * diff --git a/Civi/Api4/Service/Spec/SpecGatherer.php b/Civi/Api4/Service/Spec/SpecGatherer.php index 00fa09b503..6116c4c69d 100644 --- a/Civi/Api4/Service/Spec/SpecGatherer.php +++ b/Civi/Api4/Service/Spec/SpecGatherer.php @@ -166,7 +166,7 @@ class SpecGatherer { private function getDAOFields($entityName) { $bao = CoreUtil::getBAOFromApiName($entityName); - return $bao::fields(); + return $bao::getSupportedFields(); } } diff --git a/api/v3/utils.php b/api/v3/utils.php index 3aa58b6c8f..50268a400c 100644 --- a/api/v3/utils.php +++ b/api/v3/utils.php @@ -294,7 +294,7 @@ function _civicrm_api3_load_DAO($entity) { * return the DAO name to manipulate this function * eg. "civicrm_api3_contact_create" or "Contact" will return "CRM_Contact_BAO_Contact" * - * @return mixed|string + * @return CRM_Core_DAO|string */ function _civicrm_api3_get_DAO($name) { if (strpos($name, 'civicrm_api3') !== FALSE) { @@ -1878,15 +1878,19 @@ function _civicrm_api_get_fields($entity, $unique = FALSE, &$params = []) { if (empty($dao)) { return []; } - $d = new $dao(); - $fields = $d->fields(); + $fields = $dao::fields(); + $supportedFields = $dao::getSupportedFields(); - foreach ($fields as $name => &$field) { + foreach ($fields as $name => $field) { // Denote as core field - $field['is_core_field'] = TRUE; + $fields[$name]['is_core_field'] = TRUE; // Set html attributes for text fields if (isset($field['html'])) { - $field['html'] += (array) $d::makeAttribute($field); + $fields[$name]['html'] += (array) $dao::makeAttribute($field); + } + // Delete field if not supported by current db schema (prevents errors when there are pending db updates) + if (!isset($supportedFields[$field['name']])) { + unset($fields[$name]); } } diff --git a/sql/test_data_second_domain.mysql b/sql/test_data_second_domain.mysql index 10ec4ac1ee..a0cf296334 100644 --- a/sql/test_data_second_domain.mysql +++ b/sql/test_data_second_domain.mysql @@ -963,4 +963,4 @@ INSERT INTO civicrm_navigation VALUES ( @domainID, CONCAT('civicrm/report/instance/', @instanceID,'&reset=1'), 'Mailing Detail Report', 'Mailing Detail Report', 'administer CiviMail', 'OR', @reportlastID, '1', NULL, @instanceID+2 ); UPDATE civicrm_report_instance SET navigation_id = LAST_INSERT_ID() WHERE id = @instanceID; -UPDATE civicrm_domain SET version = '5.27.alpha1'; +UPDATE civicrm_domain SET version = '5.28.alpha1'; diff --git a/tests/phpunit/CRM/Core/DAOTest.php b/tests/phpunit/CRM/Core/DAOTest.php index 22183ac8ce..d1eb22eaa5 100644 --- a/tests/phpunit/CRM/Core/DAOTest.php +++ b/tests/phpunit/CRM/Core/DAOTest.php @@ -516,4 +516,30 @@ class CRM_Core_DAOTest extends CiviUnitTestCase { $this->fail('String not altered'); } + public function testSupportedFields() { + // Hack a different db version which will trigger getSupportedFields to filter out newer fields + \CRM_Core_DAO::$_dbColumnValueCache['CRM_Core_DAO_Domain']['id'][1]['version'] = '5.26.0'; + + $customGroupFields = CRM_Core_DAO_CustomGroup::getSupportedFields(); + // 'icon' was added in 5.28 + $this->assertArrayNotHasKey('icon', $customGroupFields); + + // Remove domain version override: + \CRM_Core_DAO::$_dbColumnValueCache = NULL; + + $activityFields = CRM_Activity_DAO_Activity::getSupportedFields(); + // Fields should be indexed by name not unique_name (which is "activity_id") + $this->assertEquals('id', $activityFields['id']['name']); + + $customGroupFields = CRM_Core_DAO_CustomGroup::getSupportedFields(); + $this->assertArrayHasKey('icon', $customGroupFields); + + \CRM_Core_Config::singleton()->userPermissionClass->permissions = ['access CiviCRM', 'view all contacts']; + $contactFields = CRM_Contact_DAO_Contact::getSupportedFields(); + $this->assertArrayHasKey('api_key', $contactFields); + + $permissionedContactFields = CRM_Contact_DAO_Contact::getSupportedFields(TRUE); + $this->assertArrayNotHasKey('api_key', $permissionedContactFields); + } + } -- 2.25.1