' . ts("WARNING: The table \"civicrm_persistent\" is flagged for removal because all official records show it being unused. However, the upgrader has detected data in this copy of \"civicrm_persistent\". Please report anything you can about the usage of this table. In the mean-time, the data will be preserved.", [ 1 => 'https://civicrm.org/bug-reporting', ]); } $config = CRM_Core_Config::singleton(); if (in_array('CiviCase', $config->enableComponents)) { // Do dry-run to get warning messages. $messages = self::_changeCaseTypeLabelToName(TRUE); foreach ($messages as $message) { $preUpgradeMessage .= "

{$message}

\n"; } } } } /** * Compute any messages which should be displayed after upgrade. * * @param string $postUpgradeMessage * alterable. * @param string $rev * an intermediate version; note that setPostUpgradeMessage is called repeatedly with different $revs. */ public function setPostUpgradeMessage(&$postUpgradeMessage, $rev) { // Example: Generate a post-upgrade message. // if ($rev == '5.12.34') { // $postUpgradeMessage .= '

' . ts("By default, CiviCRM now disables the ability to import directly from SQL. To use this feature, you must explicitly grant permission 'import SQL datasource'."); // } } /* * Important! All upgrade functions MUST add a 'runSql' task. * Uncomment and use the following template for a new upgrade version * (change the x in the function name): */ // public static function taskFoo(CRM_Queue_TaskContext $ctx, ...) { // return TRUE; // } /** * Upgrade function. * * @param string $rev */ public function upgrade_5_20_alpha1($rev) { $this->addTask('Add frontend title column to contribution page table', 'addColumn', 'civicrm_contribution_page', 'frontend_title', "varchar(255) DEFAULT NULL COMMENT 'Contribution Page Public title'", TRUE, '5.20.alpha1'); $this->addTask('Add is_template field to civicrm_contribution', 'addColumn', 'civicrm_contribution', 'is_template', "tinyint(4) DEFAULT '0' COMMENT 'Shows this is a template for recurring contributions.'", FALSE, '5.20.alpha1'); $this->addTask('Add order_reference field to civicrm_financial_trxn', 'addColumn', 'civicrm_financial_trxn', 'order_reference', "varchar(255) COMMENT 'Payment Processor external order reference'", FALSE, '5.20.alpha1'); $config = CRM_Core_Config::singleton(); if (in_array('CiviCase', $config->enableComponents)) { $this->addTask('Change direction of autoassignees in case type xml', 'changeCaseTypeAutoassignee'); $this->addTask('Change labels back to names in case type xml', 'changeCaseTypeLabelToName'); } $this->addTask(ts('Upgrade DB to %1: SQL', [1 => $rev]), 'runSql', $rev); $this->addTask('Add "Template" contribution status', 'templateStatus'); $this->addTask('Update smart groups to rename filters on case_from and case_to to case_start_date and case_end_date', 'updateSmartGroups', [ 'renameField' => [ ['old' => 'case_from_relative', 'new' => 'case_start_date_relative'], ['old' => 'case_from_start_date_high', 'new' => 'case_start_date_high'], ['old' => 'case_from_start_date_low', 'new' => 'case_start_date_low'], ['old' => 'case_to_relative', 'new' => 'case_end_date_relative'], ['old' => 'case_to_end_date_high', 'new' => 'case_end_date_high'], ['old' => 'case_to_end_date_low', 'new' => 'case_end_date_low'], ['old' => 'mailing_date_relative', 'new' => 'mailing_job_start_date_relative'], ['old' => 'mailing_date_high', 'new' => 'mailing_job_start_date_high'], ['old' => 'mailing_date_low', 'new' => 'mailing_job_start_date_low'], ['old' => 'relation_start_date_low', 'new' => 'relationship_start_date_low'], ['old' => 'relation_start_date_high', 'new' => 'relationship_start_date_high'], ['old' => 'relation_start_date_relative', 'new' => 'relationship_start_date_relative'], ['old' => 'relation_end_date_low', 'new' => 'relationship_end_date_low'], ['old' => 'relation_end_date_high', 'new' => 'relationship_end_date_high'], ['old' => 'relation_end_date_relative', 'new' => 'relationship_end_date_relative'], ['old' => 'event_start_date_low', 'new' => 'event_low'], ['old' => 'event_end_date_high', 'new' => 'event_high'], ], ]); $this->addTask('Convert Log date searches to their final names either created date or modified date', 'updateSmartGroups', [ 'renameLogFields' => [], ]); $this->addTask('Update smart groups where jcalendar fields have been converted to datepicker', 'updateSmartGroups', [ 'datepickerConversion' => [ 'birth_date', 'deceased_date', 'case_start_date', 'case_end_date', 'mailing_job_start_date', 'relationship_start_date', 'relationship_end_date', 'event', 'relation_active_period_date', 'created_date', 'modified_date', ], ]); $this->addTask('Clean up unused table "civicrm_persistent"', 'dropTableIfEmpty', 'civicrm_persistent'); $this->addTask('Convert Custom data based smart groups from jcalendar to datepicker', 'updateSmartGroups', [ 'convertCustomSmartGroups' => NULL, ]); } public static function templateStatus(CRM_Queue_TaskContext $ctx) { CRM_Core_BAO_OptionValue::ensureOptionValueExists([ 'option_group_id' => 'contribution_status', 'name' => 'Template', 'label' => ts('Template'), 'is_active' => TRUE, 'component_id' => 'CiviContribute', ]); return TRUE; } /** * Change direction of activity autoassignees in case type xml for * bidirectional relationship types if they point the other way. This is * mostly a visual issue on the case type edit screen and doesn't affect * normal operation, but could lead to confusion and a future mixup. * (dev/core#1046) * ONLY for ones using database storage - don't want to "fork" case types * that aren't currently forked. * * Earlier iterations of this used the api and array manipulation * and then another iteration used SimpleXML manipulation, but both * suffered from weirdnesses in how conversion back and forth worked. * * Here we use SQL and a regex. The thing we're changing is pretty * well-defined and unique: * N_b_a * * @return bool */ public static function changeCaseTypeAutoassignee() { self::$relationshipTypes = civicrm_api3('RelationshipType', 'get', [ 'options' => ['limit' => 0], ])['values']; // Get all case types definitions that are using db storage $dao = CRM_Core_DAO::executeQuery("SELECT id, definition FROM civicrm_case_type WHERE definition IS NOT NULL AND definition <> ''"); while ($dao->fetch()) { self::processCaseTypeAutoassignee($dao->id, $dao->definition); } return TRUE; } /** * Process a single case type * * @param $caseTypeId int * @param $definition string * xml string */ public static function processCaseTypeAutoassignee($caseTypeId, $definition) { $isDirty = FALSE; // find the autoassignees preg_match_all('/(.*?)<\/default_assignee_relationship>/', $definition, $matches); // $matches[1][n] has the text inside the xml tag, e.g. 2_a_b foreach ($matches[1] as $index => $match) { if (empty($match)) { continue; } // parse out existing id and direction list($relationshipTypeId, $direction1) = explode('_', $match); // we only care about ones that are b_a if ($direction1 === 'b') { // we only care about bidirectional if (self::isBidirectionalRelationship($relationshipTypeId)) { // flip it to be a_b // $matches[0][n] has the whole match including the xml tag $definition = str_replace($matches[0][$index], "{$relationshipTypeId}_a_b", $definition); $isDirty = TRUE; } } } if ($isDirty) { $sqlParams = [ 1 => [$definition, 'String'], 2 => [$caseTypeId, 'Integer'], ]; CRM_Core_DAO::executeQuery("UPDATE civicrm_case_type SET definition = %1 WHERE id = %2", $sqlParams); //echo "UPDATE civicrm_case_type SET definition = '" . CRM_Core_DAO::escapeString($sqlParams[1][0]) . "' WHERE id = {$sqlParams[2][0]}\n"; } } /** * Check if this is bidirectional, based on label. In the situation where * we're using this we don't care too much about the edge case where name * might not also be bidirectional. * * @param $relationshipTypeId int * * @return bool */ private static function isBidirectionalRelationship($relationshipTypeId) { if (isset(self::$relationshipTypes[$relationshipTypeId])) { if (self::$relationshipTypes[$relationshipTypeId]['label_a_b'] === self::$relationshipTypes[$relationshipTypeId]['label_b_a']) { return TRUE; } } return FALSE; } /** * Change labels in case type xml definition back to names. (dev/core#1046) * ONLY for ones using database storage - don't want to "fork" case types * that aren't currently forked. * * @return bool */ public static function changeCaseTypeLabelToName() { self::_changeCaseTypeLabelToName(FALSE); return TRUE; } /** * Change labels in case type xml definition back to names. (dev/core#1046) * ONLY for ones using database storage - don't want to "fork" case types * that aren't currently forked. * * @param $isDryRun bool * If TRUE then don't actually change anything just report warnings. * * @return array List of warning messages. */ public static function _changeCaseTypeLabelToName($isDryRun = FALSE) { $messages = []; self::$relationshipTypes = civicrm_api3('RelationshipType', 'get', [ 'options' => ['limit' => 0], ])['values']; // Get all case types definitions that are using db storage $dao = CRM_Core_DAO::executeQuery("SELECT id FROM civicrm_case_type WHERE definition IS NOT NULL AND definition <> ''"); while ($dao->fetch()) { // array_merge so that existing numeric keys don't get overwritten $messages = array_merge($messages, self::_processCaseTypeLabelName($isDryRun, $dao->id)); } return $messages; } /** * Process a single case type for _changeCaseTypeLabelToName() * * @param $isDryRun bool * If TRUE then don't actually change anything just report warnings. * @param $caseTypeId int */ private static function _processCaseTypeLabelName($isDryRun, $caseTypeId) { $messages = []; $isDirty = FALSE; // Get the case type definition $caseType = civicrm_api3( 'CaseType', 'get', ['id' => $caseTypeId] )['values'][$caseTypeId]; foreach ($caseType['definition']['caseRoles'] as $roleSequenceId => $role) { // First double-check that there is a unique match on label so we // don't get it wrong. // There's maybe a fancy way to do this with array_XXX functions but // need to take into account edge cases where bidirectional but name // is different, or where somehow two labels are the same across types, // so do old-fashioned loop. $cantConvertMessage = NULL; $foundName = NULL; foreach (self::$relationshipTypes as $relationshipType) { // does it match one of our existing labels if ($relationshipType['label_a_b'] === $role['name'] || $relationshipType['label_b_a'] === $role['name']) { // So either it's ambiguous, in which case exit loop with a message, // or we have the name, so exit loop with that. $cantConvertMessage = self::checkAmbiguous($relationshipType, $caseType['name'], $role['name']); if (empty($cantConvertMessage)) { // not ambiguous, so note the corresponding name for the direction $foundName = ($relationshipType['label_a_b'] === $role['name']) ? $relationshipType['name_a_b'] : $relationshipType['name_b_a']; } break; } } if (empty($foundName) && empty($cantConvertMessage)) { // It's possible we went through all relationship types and didn't // find any match, so don't change anything. $cantConvertMessage = ts("Case Type '%1', role '%2' doesn't seem to be a valid role. See the administration console status messages for more info.", [ 1 => htmlspecialchars($caseType['name']), 2 => htmlspecialchars($role['name']), ]); } // Only two possibilities now are we have a name, or we have a message. // So the if($foundName) is redundant, but seems clearer somehow. if ($foundName && empty($cantConvertMessage)) { // If name and label are the same don't need to update anything. if ($foundName !== $role['name']) { $caseType['definition']['caseRoles'][$roleSequenceId]['name'] = $foundName; $isDirty = TRUE; } } else { $messages[] = $cantConvertMessage; } // end looping thru all roles in definition } // If this is a dry run during preupgrade checks we can skip this and // just return any messages. // If for real, then update the case type and here if there's errors // we don't really have a choice but to stop the entire upgrade // completely. There's no way to just send back messages during a queue // run. But we can log a message to error log so that the user has a // little more specific info about which case type. if ($isDirty && !$isDryRun) { $exception = NULL; try { $api_result = civicrm_api3('CaseType', 'create', $caseType); } catch (Exception $e) { $exception = $e; $errorMessage = ts("Error updating case type '%1': %2", [ 1 => htmlspecialchars($caseType['name']), 2 => htmlspecialchars($e->getMessage()), ]); CRM_Core_Error::debug_log_message($errorMessage); } if (!empty($api_result['is_error'])) { $errorMessage = ts("Error updating case type '%1': %2", [ 1 => htmlspecialchars($caseType['name']), 2 => htmlspecialchars($api_result['error_message']), ]); CRM_Core_Error::debug_log_message($errorMessage); $exception = new Exception($errorMessage); } // We need to rethrow the error which unfortunately stops the // entire upgrade including any further tasks. But otherwise // the only way to notify the user something went wrong is with a // crazy workaround. if ($exception) { throw $exception; } } return $messages; } /** * Helper for _processCaseTypeLabelName to check if a label can't be * converted unambiguously to name. * * If it's bidirectional, we can't convert it if there's an edge case * where the two names are different. * * If it's unidirectional, we can't convert it if there's an edge case * where there's another type that has the same label. * * @param $relationshipType array * @param $caseTypeName string * @param $xmlRoleName string * * @return string|NULL */ private static function checkAmbiguous($relationshipType, $caseTypeName, $xmlRoleName) { $cantConvertMessage = NULL; if ($relationshipType['label_a_b'] === $relationshipType['label_b_a']) { // bidirectional, so check if names are different for some reason if ($relationshipType['name_a_b'] !== $relationshipType['name_b_a']) { $cantConvertMessage = ts("Case Type '%1', role '%2' has an ambiguous configuration and can't be automatically updated. See the administration console status messages for more info.", [ 1 => htmlspecialchars($caseTypeName), 2 => htmlspecialchars($xmlRoleName), ]); } } else { // Check if it matches either label_a_b or label_b_a for another type foreach (self::$relationshipTypes as $innerLoopId => $innerLoopType) { if ($innerLoopId == $relationshipType['id']) { // Only check types that aren't the same one we're on. // Sidenote: The loop index is integer but the 'id' member is string continue; } if ($innerLoopType['label_a_b'] === $xmlRoleName || $innerLoopType['label_b_a'] === $xmlRoleName) { $cantConvertMessage = ts("Case Type '%1', role '%2' has an ambiguous configuration where the role matches multiple labels and so can't be automatically updated. See the administration console status messages for more info.", [ 1 => htmlspecialchars($caseTypeName), 2 => htmlspecialchars($xmlRoleName), ]); break; } } } return $cantConvertMessage; } }