Commit | Line | Data |
---|---|---|
187fc9e9 C |
1 | <?php |
2 | /* | |
3 | +--------------------------------------------------------------------+ | |
bc77d7c0 | 4 | | Copyright CiviCRM LLC. All rights reserved. | |
187fc9e9 | 5 | | | |
bc77d7c0 TO |
6 | | This work is published under the GNU AGPLv3 license with some | |
7 | | permitted exceptions and without any warranty. For full license | | |
8 | | and copyright information, see https://civicrm.org/licensing | | |
187fc9e9 C |
9 | +--------------------------------------------------------------------+ |
10 | */ | |
11 | ||
12 | /** | |
6317d291 CW |
13 | * Upgrade logic for FiveTwenty |
14 | */ | |
187fc9e9 C |
15 | class CRM_Upgrade_Incremental_php_FiveTwenty extends CRM_Upgrade_Incremental_Base { |
16 | ||
400db156 | 17 | /** |
51dda21e | 18 | * @var array |
400db156 D |
19 | * api call result keyed on relationship_type.id |
20 | */ | |
21 | protected static $relationshipTypes; | |
22 | ||
187fc9e9 C |
23 | /** |
24 | * Compute any messages which should be displayed beforeupgrade. | |
25 | * | |
26 | * Note: This function is called iteratively for each upcoming | |
27 | * revision to the database. | |
28 | * | |
29 | * @param string $preUpgradeMessage | |
30 | * @param string $rev | |
31 | * a version number, e.g. '4.4.alpha1', '4.4.beta3', '4.4.0'. | |
32 | * @param null $currentVer | |
33 | */ | |
34 | public function setPreUpgradeMessage(&$preUpgradeMessage, $rev, $currentVer = NULL) { | |
81633334 TO |
35 | if ($rev == '5.20.alpha1') { |
36 | if (CRM_Core_DAO::checkTableExists('civicrm_persistent') && CRM_Core_DAO::checkTableHasData('civicrm_persistent')) { | |
37 | $preUpgradeMessage .= '<br/>' . ts("WARNING: The table \"<code>civicrm_persistent</code>\" is flagged for removal because all official records show it being unused. However, the upgrader has detected data in this copy of \"<code>civicrm_persistent</code>\". Please <a href='%1' target='_blank'>report</a> anything you can about the usage of this table. In the mean-time, the data will be preserved.", [ | |
38 | 1 => 'https://civicrm.org/bug-reporting', | |
39 | ]); | |
40 | } | |
9649c2a4 D |
41 | |
42 | $config = CRM_Core_Config::singleton(); | |
43 | if (in_array('CiviCase', $config->enableComponents)) { | |
44 | // Do dry-run to get warning messages. | |
45 | $messages = self::_changeCaseTypeLabelToName(TRUE); | |
46 | foreach ($messages as $message) { | |
47 | $preUpgradeMessage .= "<p>{$message}</p>\n"; | |
48 | } | |
49 | } | |
81633334 | 50 | } |
187fc9e9 C |
51 | } |
52 | ||
6489e3de SL |
53 | /** |
54 | * Upgrade function. | |
55 | * | |
56 | * @param string $rev | |
57 | */ | |
58 | public function upgrade_5_20_alpha1($rev) { | |
59 | $this->addTask('Add frontend title column to contribution page table', 'addColumn', 'civicrm_contribution_page', | |
60 | 'frontend_title', "varchar(255) DEFAULT NULL COMMENT 'Contribution Page Public title'", TRUE, '5.20.alpha1'); | |
dc396835 AS |
61 | $this->addTask('Add is_template field to civicrm_contribution', 'addColumn', 'civicrm_contribution', 'is_template', |
62 | "tinyint(4) DEFAULT '0' COMMENT 'Shows this is a template for recurring contributions.'", FALSE, '5.20.alpha1'); | |
1f34d30a AS |
63 | $this->addTask('Add order_reference field to civicrm_financial_trxn', 'addColumn', 'civicrm_financial_trxn', 'order_reference', |
64 | "varchar(255) COMMENT 'Payment Processor external order reference'", FALSE, '5.20.alpha1'); | |
400db156 D |
65 | $config = CRM_Core_Config::singleton(); |
66 | if (in_array('CiviCase', $config->enableComponents)) { | |
67 | $this->addTask('Change direction of autoassignees in case type xml', 'changeCaseTypeAutoassignee'); | |
9649c2a4 | 68 | $this->addTask('Change labels back to names in case type xml', 'changeCaseTypeLabelToName'); |
400db156 | 69 | } |
6489e3de | 70 | $this->addTask(ts('Upgrade DB to %1: SQL', [1 => $rev]), 'runSql', $rev); |
f831ac20 | 71 | $this->addTask('Add "Template" contribution status', 'templateStatus'); |
81633334 | 72 | $this->addTask('Clean up unused table "civicrm_persistent"', 'dropTableIfEmpty', 'civicrm_persistent'); |
f831ac20 EE |
73 | } |
74 | ||
75 | public static function templateStatus(CRM_Queue_TaskContext $ctx) { | |
76 | CRM_Core_BAO_OptionValue::ensureOptionValueExists([ | |
77 | 'option_group_id' => 'contribution_status', | |
78 | 'name' => 'Template', | |
79 | 'label' => ts('Template'), | |
80 | 'is_active' => TRUE, | |
81 | 'component_id' => 'CiviContribute', | |
82 | ]); | |
83 | return TRUE; | |
6489e3de SL |
84 | } |
85 | ||
400db156 D |
86 | /** |
87 | * Change direction of activity autoassignees in case type xml for | |
88 | * bidirectional relationship types if they point the other way. This is | |
89 | * mostly a visual issue on the case type edit screen and doesn't affect | |
90 | * normal operation, but could lead to confusion and a future mixup. | |
91 | * (dev/core#1046) | |
92 | * ONLY for ones using database storage - don't want to "fork" case types | |
93 | * that aren't currently forked. | |
94 | * | |
95 | * Earlier iterations of this used the api and array manipulation | |
96 | * and then another iteration used SimpleXML manipulation, but both | |
97 | * suffered from weirdnesses in how conversion back and forth worked. | |
98 | * | |
99 | * Here we use SQL and a regex. The thing we're changing is pretty | |
100 | * well-defined and unique: | |
101 | * <default_assignee_relationship>N_b_a</default_assignee_relationship> | |
102 | * | |
103 | * @return bool | |
104 | */ | |
105 | public static function changeCaseTypeAutoassignee() { | |
106 | self::$relationshipTypes = civicrm_api3('RelationshipType', 'get', [ | |
107 | 'options' => ['limit' => 0], | |
108 | ])['values']; | |
109 | ||
110 | // Get all case types definitions that are using db storage | |
111 | $dao = CRM_Core_DAO::executeQuery("SELECT id, definition FROM civicrm_case_type WHERE definition IS NOT NULL AND definition <> ''"); | |
112 | while ($dao->fetch()) { | |
113 | self::processCaseTypeAutoassignee($dao->id, $dao->definition); | |
114 | } | |
115 | return TRUE; | |
116 | } | |
117 | ||
118 | /** | |
119 | * Process a single case type | |
120 | * | |
e0f5b841 BT |
121 | * @param int $caseTypeId |
122 | * @param string $definition | |
400db156 D |
123 | * xml string |
124 | */ | |
125 | public static function processCaseTypeAutoassignee($caseTypeId, $definition) { | |
126 | $isDirty = FALSE; | |
127 | // find the autoassignees | |
128 | preg_match_all('/<default_assignee_relationship>(.*?)<\/default_assignee_relationship>/', $definition, $matches); | |
129 | // $matches[1][n] has the text inside the xml tag, e.g. 2_a_b | |
130 | foreach ($matches[1] as $index => $match) { | |
131 | if (empty($match)) { | |
132 | continue; | |
133 | } | |
134 | // parse out existing id and direction | |
135 | list($relationshipTypeId, $direction1) = explode('_', $match); | |
136 | // we only care about ones that are b_a | |
137 | if ($direction1 === 'b') { | |
138 | // we only care about bidirectional | |
139 | if (self::isBidirectionalRelationship($relationshipTypeId)) { | |
140 | // flip it to be a_b | |
141 | // $matches[0][n] has the whole match including the xml tag | |
142 | $definition = str_replace($matches[0][$index], "<default_assignee_relationship>{$relationshipTypeId}_a_b</default_assignee_relationship>", $definition); | |
143 | $isDirty = TRUE; | |
144 | } | |
145 | } | |
146 | } | |
147 | ||
148 | if ($isDirty) { | |
149 | $sqlParams = [ | |
150 | 1 => [$definition, 'String'], | |
151 | 2 => [$caseTypeId, 'Integer'], | |
152 | ]; | |
153 | CRM_Core_DAO::executeQuery("UPDATE civicrm_case_type SET definition = %1 WHERE id = %2", $sqlParams); | |
154 | //echo "UPDATE civicrm_case_type SET definition = '" . CRM_Core_DAO::escapeString($sqlParams[1][0]) . "' WHERE id = {$sqlParams[2][0]}\n"; | |
155 | } | |
156 | } | |
157 | ||
158 | /** | |
159 | * Check if this is bidirectional, based on label. In the situation where | |
160 | * we're using this we don't care too much about the edge case where name | |
161 | * might not also be bidirectional. | |
162 | * | |
a2f24340 | 163 | * @param int $relationshipTypeId |
400db156 D |
164 | * |
165 | * @return bool | |
166 | */ | |
167 | private static function isBidirectionalRelationship($relationshipTypeId) { | |
168 | if (isset(self::$relationshipTypes[$relationshipTypeId])) { | |
169 | if (self::$relationshipTypes[$relationshipTypeId]['label_a_b'] === self::$relationshipTypes[$relationshipTypeId]['label_b_a']) { | |
170 | return TRUE; | |
171 | } | |
172 | } | |
173 | return FALSE; | |
174 | } | |
175 | ||
9649c2a4 D |
176 | /** |
177 | * Change labels in case type xml definition back to names. (dev/core#1046) | |
178 | * ONLY for ones using database storage - don't want to "fork" case types | |
179 | * that aren't currently forked. | |
180 | * | |
181 | * @return bool | |
182 | */ | |
183 | public static function changeCaseTypeLabelToName() { | |
184 | self::_changeCaseTypeLabelToName(FALSE); | |
185 | return TRUE; | |
186 | } | |
187 | ||
188 | /** | |
189 | * Change labels in case type xml definition back to names. (dev/core#1046) | |
190 | * ONLY for ones using database storage - don't want to "fork" case types | |
191 | * that aren't currently forked. | |
192 | * | |
a2f24340 | 193 | * @param bool $isDryRun |
9649c2a4 D |
194 | * If TRUE then don't actually change anything just report warnings. |
195 | * | |
196 | * @return array List of warning messages. | |
197 | */ | |
198 | public static function _changeCaseTypeLabelToName($isDryRun = FALSE) { | |
199 | $messages = []; | |
200 | self::$relationshipTypes = civicrm_api3('RelationshipType', 'get', [ | |
201 | 'options' => ['limit' => 0], | |
202 | ])['values']; | |
203 | ||
204 | // Get all case types definitions that are using db storage | |
205 | $dao = CRM_Core_DAO::executeQuery("SELECT id FROM civicrm_case_type WHERE definition IS NOT NULL AND definition <> ''"); | |
206 | while ($dao->fetch()) { | |
207 | // array_merge so that existing numeric keys don't get overwritten | |
208 | $messages = array_merge($messages, self::_processCaseTypeLabelName($isDryRun, $dao->id)); | |
209 | } | |
210 | return $messages; | |
211 | } | |
212 | ||
213 | /** | |
214 | * Process a single case type for _changeCaseTypeLabelToName() | |
215 | * | |
e0f5b841 | 216 | * @param bool $isDryRun |
9649c2a4 | 217 | * If TRUE then don't actually change anything just report warnings. |
e0f5b841 | 218 | * @param int $caseTypeId |
9649c2a4 D |
219 | */ |
220 | private static function _processCaseTypeLabelName($isDryRun, $caseTypeId) { | |
221 | $messages = []; | |
222 | $isDirty = FALSE; | |
223 | ||
224 | // Get the case type definition | |
225 | $caseType = civicrm_api3( | |
226 | 'CaseType', | |
227 | 'get', | |
228 | ['id' => $caseTypeId] | |
229 | )['values'][$caseTypeId]; | |
230 | ||
231 | foreach ($caseType['definition']['caseRoles'] as $roleSequenceId => $role) { | |
232 | // First double-check that there is a unique match on label so we | |
233 | // don't get it wrong. | |
234 | // There's maybe a fancy way to do this with array_XXX functions but | |
235 | // need to take into account edge cases where bidirectional but name | |
236 | // is different, or where somehow two labels are the same across types, | |
237 | // so do old-fashioned loop. | |
238 | ||
239 | $cantConvertMessage = NULL; | |
240 | $foundName = NULL; | |
241 | foreach (self::$relationshipTypes as $relationshipType) { | |
242 | // does it match one of our existing labels | |
243 | if ($relationshipType['label_a_b'] === $role['name'] || $relationshipType['label_b_a'] === $role['name']) { | |
244 | // So either it's ambiguous, in which case exit loop with a message, | |
245 | // or we have the name, so exit loop with that. | |
246 | $cantConvertMessage = self::checkAmbiguous($relationshipType, $caseType['name'], $role['name']); | |
247 | if (empty($cantConvertMessage)) { | |
248 | // not ambiguous, so note the corresponding name for the direction | |
249 | $foundName = ($relationshipType['label_a_b'] === $role['name']) ? $relationshipType['name_a_b'] : $relationshipType['name_b_a']; | |
250 | } | |
251 | break; | |
252 | } | |
253 | } | |
254 | ||
255 | if (empty($foundName) && empty($cantConvertMessage)) { | |
256 | // It's possible we went through all relationship types and didn't | |
257 | // find any match, so don't change anything. | |
258 | $cantConvertMessage = ts("Case Type '%1', role '%2' doesn't seem to be a valid role. See the administration console status messages for more info.", [ | |
259 | 1 => htmlspecialchars($caseType['name']), | |
260 | 2 => htmlspecialchars($role['name']), | |
261 | ]); | |
262 | } | |
263 | // Only two possibilities now are we have a name, or we have a message. | |
264 | // So the if($foundName) is redundant, but seems clearer somehow. | |
265 | if ($foundName && empty($cantConvertMessage)) { | |
266 | // If name and label are the same don't need to update anything. | |
267 | if ($foundName !== $role['name']) { | |
268 | $caseType['definition']['caseRoles'][$roleSequenceId]['name'] = $foundName; | |
269 | $isDirty = TRUE; | |
270 | } | |
271 | } | |
272 | else { | |
273 | $messages[] = $cantConvertMessage; | |
274 | } | |
275 | ||
276 | // end looping thru all roles in definition | |
277 | } | |
278 | ||
279 | // If this is a dry run during preupgrade checks we can skip this and | |
280 | // just return any messages. | |
281 | // If for real, then update the case type and here if there's errors | |
282 | // we don't really have a choice but to stop the entire upgrade | |
283 | // completely. There's no way to just send back messages during a queue | |
284 | // run. But we can log a message to error log so that the user has a | |
285 | // little more specific info about which case type. | |
286 | if ($isDirty && !$isDryRun) { | |
287 | $exception = NULL; | |
288 | try { | |
289 | $api_result = civicrm_api3('CaseType', 'create', $caseType); | |
290 | } | |
291 | catch (Exception $e) { | |
292 | $exception = $e; | |
293 | $errorMessage = ts("Error updating case type '%1': %2", [ | |
294 | 1 => htmlspecialchars($caseType['name']), | |
295 | 2 => htmlspecialchars($e->getMessage()), | |
296 | ]); | |
297 | CRM_Core_Error::debug_log_message($errorMessage); | |
298 | } | |
299 | if (!empty($api_result['is_error'])) { | |
300 | $errorMessage = ts("Error updating case type '%1': %2", [ | |
301 | 1 => htmlspecialchars($caseType['name']), | |
302 | 2 => htmlspecialchars($api_result['error_message']), | |
303 | ]); | |
304 | CRM_Core_Error::debug_log_message($errorMessage); | |
305 | $exception = new Exception($errorMessage); | |
306 | } | |
307 | // We need to rethrow the error which unfortunately stops the | |
308 | // entire upgrade including any further tasks. But otherwise | |
309 | // the only way to notify the user something went wrong is with a | |
310 | // crazy workaround. | |
311 | if ($exception) { | |
312 | throw $exception; | |
313 | } | |
314 | } | |
315 | ||
316 | return $messages; | |
317 | } | |
318 | ||
319 | /** | |
320 | * Helper for _processCaseTypeLabelName to check if a label can't be | |
321 | * converted unambiguously to name. | |
322 | * | |
323 | * If it's bidirectional, we can't convert it if there's an edge case | |
324 | * where the two names are different. | |
325 | * | |
326 | * If it's unidirectional, we can't convert it if there's an edge case | |
327 | * where there's another type that has the same label. | |
328 | * | |
a2f24340 BT |
329 | * @param array $relationshipType |
330 | * @param string $caseTypeName | |
331 | * @param string $xmlRoleName | |
9649c2a4 D |
332 | * |
333 | * @return string|NULL | |
334 | */ | |
335 | private static function checkAmbiguous($relationshipType, $caseTypeName, $xmlRoleName) { | |
336 | $cantConvertMessage = NULL; | |
337 | if ($relationshipType['label_a_b'] === $relationshipType['label_b_a']) { | |
338 | // bidirectional, so check if names are different for some reason | |
339 | if ($relationshipType['name_a_b'] !== $relationshipType['name_b_a']) { | |
340 | $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.", [ | |
341 | 1 => htmlspecialchars($caseTypeName), | |
342 | 2 => htmlspecialchars($xmlRoleName), | |
343 | ]); | |
344 | } | |
345 | } | |
346 | else { | |
347 | // Check if it matches either label_a_b or label_b_a for another type | |
348 | foreach (self::$relationshipTypes as $innerLoopId => $innerLoopType) { | |
349 | if ($innerLoopId == $relationshipType['id']) { | |
350 | // Only check types that aren't the same one we're on. | |
351 | // Sidenote: The loop index is integer but the 'id' member is string | |
352 | continue; | |
353 | } | |
354 | if ($innerLoopType['label_a_b'] === $xmlRoleName || $innerLoopType['label_b_a'] === $xmlRoleName) { | |
355 | $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.", [ | |
356 | 1 => htmlspecialchars($caseTypeName), | |
357 | 2 => htmlspecialchars($xmlRoleName), | |
358 | ]); | |
359 | break; | |
360 | } | |
361 | } | |
362 | } | |
363 | return $cantConvertMessage; | |
364 | } | |
365 | ||
187fc9e9 | 366 | } |