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 | /** | |
13 | * Upgrade logic for FiveTwenty */ | |
14 | class CRM_Upgrade_Incremental_php_FiveTwenty extends CRM_Upgrade_Incremental_Base { | |
15 | ||
400db156 | 16 | /** |
51dda21e | 17 | * @var array |
400db156 D |
18 | * api call result keyed on relationship_type.id |
19 | */ | |
20 | protected static $relationshipTypes; | |
21 | ||
187fc9e9 C |
22 | /** |
23 | * Compute any messages which should be displayed beforeupgrade. | |
24 | * | |
25 | * Note: This function is called iteratively for each upcoming | |
26 | * revision to the database. | |
27 | * | |
28 | * @param string $preUpgradeMessage | |
29 | * @param string $rev | |
30 | * a version number, e.g. '4.4.alpha1', '4.4.beta3', '4.4.0'. | |
31 | * @param null $currentVer | |
32 | */ | |
33 | public function setPreUpgradeMessage(&$preUpgradeMessage, $rev, $currentVer = NULL) { | |
81633334 TO |
34 | if ($rev == '5.20.alpha1') { |
35 | if (CRM_Core_DAO::checkTableExists('civicrm_persistent') && CRM_Core_DAO::checkTableHasData('civicrm_persistent')) { | |
36 | $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.", [ | |
37 | 1 => 'https://civicrm.org/bug-reporting', | |
38 | ]); | |
39 | } | |
9649c2a4 D |
40 | |
41 | $config = CRM_Core_Config::singleton(); | |
42 | if (in_array('CiviCase', $config->enableComponents)) { | |
43 | // Do dry-run to get warning messages. | |
44 | $messages = self::_changeCaseTypeLabelToName(TRUE); | |
45 | foreach ($messages as $message) { | |
46 | $preUpgradeMessage .= "<p>{$message}</p>\n"; | |
47 | } | |
48 | } | |
81633334 | 49 | } |
187fc9e9 C |
50 | } |
51 | ||
52 | /** | |
53 | * Compute any messages which should be displayed after upgrade. | |
54 | * | |
55 | * @param string $postUpgradeMessage | |
56 | * alterable. | |
57 | * @param string $rev | |
58 | * an intermediate version; note that setPostUpgradeMessage is called repeatedly with different $revs. | |
59 | */ | |
60 | public function setPostUpgradeMessage(&$postUpgradeMessage, $rev) { | |
61 | // Example: Generate a post-upgrade message. | |
62 | // if ($rev == '5.12.34') { | |
63 | // $postUpgradeMessage .= '<br /><br />' . 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'."); | |
64 | // } | |
65 | } | |
66 | ||
67 | /* | |
68 | * Important! All upgrade functions MUST add a 'runSql' task. | |
69 | * Uncomment and use the following template for a new upgrade version | |
70 | * (change the x in the function name): | |
71 | */ | |
72 | ||
187fc9e9 C |
73 | // public static function taskFoo(CRM_Queue_TaskContext $ctx, ...) { |
74 | // return TRUE; | |
75 | // } | |
76 | ||
6489e3de SL |
77 | /** |
78 | * Upgrade function. | |
79 | * | |
80 | * @param string $rev | |
81 | */ | |
82 | public function upgrade_5_20_alpha1($rev) { | |
83 | $this->addTask('Add frontend title column to contribution page table', 'addColumn', 'civicrm_contribution_page', | |
84 | 'frontend_title', "varchar(255) DEFAULT NULL COMMENT 'Contribution Page Public title'", TRUE, '5.20.alpha1'); | |
dc396835 AS |
85 | $this->addTask('Add is_template field to civicrm_contribution', 'addColumn', 'civicrm_contribution', 'is_template', |
86 | "tinyint(4) DEFAULT '0' COMMENT 'Shows this is a template for recurring contributions.'", FALSE, '5.20.alpha1'); | |
1f34d30a AS |
87 | $this->addTask('Add order_reference field to civicrm_financial_trxn', 'addColumn', 'civicrm_financial_trxn', 'order_reference', |
88 | "varchar(255) COMMENT 'Payment Processor external order reference'", FALSE, '5.20.alpha1'); | |
400db156 D |
89 | $config = CRM_Core_Config::singleton(); |
90 | if (in_array('CiviCase', $config->enableComponents)) { | |
91 | $this->addTask('Change direction of autoassignees in case type xml', 'changeCaseTypeAutoassignee'); | |
9649c2a4 | 92 | $this->addTask('Change labels back to names in case type xml', 'changeCaseTypeLabelToName'); |
400db156 | 93 | } |
6489e3de | 94 | $this->addTask(ts('Upgrade DB to %1: SQL', [1 => $rev]), 'runSql', $rev); |
f831ac20 | 95 | $this->addTask('Add "Template" contribution status', 'templateStatus'); |
7d832c0c SL |
96 | $this->addTask('Update smart groups to rename filters on case_from and case_to to case_start_date and case_end_date', 'updateSmartGroups', [ |
97 | 'renameField' => [ | |
98 | ['old' => 'case_from_relative', 'new' => 'case_start_date_relative'], | |
99 | ['old' => 'case_from_start_date_high', 'new' => 'case_start_date_high'], | |
100 | ['old' => 'case_from_start_date_low', 'new' => 'case_start_date_low'], | |
101 | ['old' => 'case_to_relative', 'new' => 'case_end_date_relative'], | |
102 | ['old' => 'case_to_end_date_high', 'new' => 'case_end_date_high'], | |
103 | ['old' => 'case_to_end_date_low', 'new' => 'case_end_date_low'], | |
0058ef3f SL |
104 | ['old' => 'mailing_date_relative', 'new' => 'mailing_job_start_date_relative'], |
105 | ['old' => 'mailing_date_high', 'new' => 'mailing_job_start_date_high'], | |
106 | ['old' => 'mailing_date_low', 'new' => 'mailing_job_start_date_low'], | |
41b8dd1d | 107 | ['old' => 'relation_start_date_low', 'new' => 'relationship_start_date_low'], |
108 | ['old' => 'relation_start_date_high', 'new' => 'relationship_start_date_high'], | |
109 | ['old' => 'relation_start_date_relative', 'new' => 'relationship_start_date_relative'], | |
110 | ['old' => 'relation_end_date_low', 'new' => 'relationship_end_date_low'], | |
111 | ['old' => 'relation_end_date_high', 'new' => 'relationship_end_date_high'], | |
112 | ['old' => 'relation_end_date_relative', 'new' => 'relationship_end_date_relative'], | |
c1436807 SL |
113 | ['old' => 'event_start_date_low', 'new' => 'event_low'], |
114 | ['old' => 'event_end_date_high', 'new' => 'event_high'], | |
7d832c0c SL |
115 | ], |
116 | ]); | |
70e0d21f SL |
117 | $this->addTask('Convert Log date searches to their final names either created date or modified date', 'updateSmartGroups', [ |
118 | 'renameLogFields' => [], | |
119 | ]); | |
7d832c0c SL |
120 | $this->addTask('Update smart groups where jcalendar fields have been converted to datepicker', 'updateSmartGroups', [ |
121 | 'datepickerConversion' => [ | |
bca2ad39 | 122 | 'birth_date', |
123 | 'deceased_date', | |
7d832c0c SL |
124 | 'case_start_date', |
125 | 'case_end_date', | |
0058ef3f | 126 | 'mailing_job_start_date', |
41b8dd1d | 127 | 'relationship_start_date', |
128 | 'relationship_end_date', | |
c1436807 SL |
129 | 'event', |
130 | 'relation_active_period_date', | |
70e0d21f SL |
131 | 'created_date', |
132 | 'modified_date', | |
7d832c0c SL |
133 | ], |
134 | ]); | |
81633334 | 135 | $this->addTask('Clean up unused table "civicrm_persistent"', 'dropTableIfEmpty', 'civicrm_persistent'); |
7f054690 SL |
136 | $this->addTask('Convert Custom data based smart groups from jcalendar to datepicker', 'updateSmartGroups', [ |
137 | 'convertCustomSmartGroups' => NULL, | |
138 | ]); | |
f831ac20 EE |
139 | } |
140 | ||
141 | public static function templateStatus(CRM_Queue_TaskContext $ctx) { | |
142 | CRM_Core_BAO_OptionValue::ensureOptionValueExists([ | |
143 | 'option_group_id' => 'contribution_status', | |
144 | 'name' => 'Template', | |
145 | 'label' => ts('Template'), | |
146 | 'is_active' => TRUE, | |
147 | 'component_id' => 'CiviContribute', | |
148 | ]); | |
149 | return TRUE; | |
6489e3de SL |
150 | } |
151 | ||
400db156 D |
152 | /** |
153 | * Change direction of activity autoassignees in case type xml for | |
154 | * bidirectional relationship types if they point the other way. This is | |
155 | * mostly a visual issue on the case type edit screen and doesn't affect | |
156 | * normal operation, but could lead to confusion and a future mixup. | |
157 | * (dev/core#1046) | |
158 | * ONLY for ones using database storage - don't want to "fork" case types | |
159 | * that aren't currently forked. | |
160 | * | |
161 | * Earlier iterations of this used the api and array manipulation | |
162 | * and then another iteration used SimpleXML manipulation, but both | |
163 | * suffered from weirdnesses in how conversion back and forth worked. | |
164 | * | |
165 | * Here we use SQL and a regex. The thing we're changing is pretty | |
166 | * well-defined and unique: | |
167 | * <default_assignee_relationship>N_b_a</default_assignee_relationship> | |
168 | * | |
169 | * @return bool | |
170 | */ | |
171 | public static function changeCaseTypeAutoassignee() { | |
172 | self::$relationshipTypes = civicrm_api3('RelationshipType', 'get', [ | |
173 | 'options' => ['limit' => 0], | |
174 | ])['values']; | |
175 | ||
176 | // Get all case types definitions that are using db storage | |
177 | $dao = CRM_Core_DAO::executeQuery("SELECT id, definition FROM civicrm_case_type WHERE definition IS NOT NULL AND definition <> ''"); | |
178 | while ($dao->fetch()) { | |
179 | self::processCaseTypeAutoassignee($dao->id, $dao->definition); | |
180 | } | |
181 | return TRUE; | |
182 | } | |
183 | ||
184 | /** | |
185 | * Process a single case type | |
186 | * | |
187 | * @param $caseTypeId int | |
188 | * @param $definition string | |
189 | * xml string | |
190 | */ | |
191 | public static function processCaseTypeAutoassignee($caseTypeId, $definition) { | |
192 | $isDirty = FALSE; | |
193 | // find the autoassignees | |
194 | preg_match_all('/<default_assignee_relationship>(.*?)<\/default_assignee_relationship>/', $definition, $matches); | |
195 | // $matches[1][n] has the text inside the xml tag, e.g. 2_a_b | |
196 | foreach ($matches[1] as $index => $match) { | |
197 | if (empty($match)) { | |
198 | continue; | |
199 | } | |
200 | // parse out existing id and direction | |
201 | list($relationshipTypeId, $direction1) = explode('_', $match); | |
202 | // we only care about ones that are b_a | |
203 | if ($direction1 === 'b') { | |
204 | // we only care about bidirectional | |
205 | if (self::isBidirectionalRelationship($relationshipTypeId)) { | |
206 | // flip it to be a_b | |
207 | // $matches[0][n] has the whole match including the xml tag | |
208 | $definition = str_replace($matches[0][$index], "<default_assignee_relationship>{$relationshipTypeId}_a_b</default_assignee_relationship>", $definition); | |
209 | $isDirty = TRUE; | |
210 | } | |
211 | } | |
212 | } | |
213 | ||
214 | if ($isDirty) { | |
215 | $sqlParams = [ | |
216 | 1 => [$definition, 'String'], | |
217 | 2 => [$caseTypeId, 'Integer'], | |
218 | ]; | |
219 | CRM_Core_DAO::executeQuery("UPDATE civicrm_case_type SET definition = %1 WHERE id = %2", $sqlParams); | |
220 | //echo "UPDATE civicrm_case_type SET definition = '" . CRM_Core_DAO::escapeString($sqlParams[1][0]) . "' WHERE id = {$sqlParams[2][0]}\n"; | |
221 | } | |
222 | } | |
223 | ||
224 | /** | |
225 | * Check if this is bidirectional, based on label. In the situation where | |
226 | * we're using this we don't care too much about the edge case where name | |
227 | * might not also be bidirectional. | |
228 | * | |
229 | * @param $relationshipTypeId int | |
230 | * | |
231 | * @return bool | |
232 | */ | |
233 | private static function isBidirectionalRelationship($relationshipTypeId) { | |
234 | if (isset(self::$relationshipTypes[$relationshipTypeId])) { | |
235 | if (self::$relationshipTypes[$relationshipTypeId]['label_a_b'] === self::$relationshipTypes[$relationshipTypeId]['label_b_a']) { | |
236 | return TRUE; | |
237 | } | |
238 | } | |
239 | return FALSE; | |
240 | } | |
241 | ||
9649c2a4 D |
242 | /** |
243 | * Change labels in case type xml definition back to names. (dev/core#1046) | |
244 | * ONLY for ones using database storage - don't want to "fork" case types | |
245 | * that aren't currently forked. | |
246 | * | |
247 | * @return bool | |
248 | */ | |
249 | public static function changeCaseTypeLabelToName() { | |
250 | self::_changeCaseTypeLabelToName(FALSE); | |
251 | return TRUE; | |
252 | } | |
253 | ||
254 | /** | |
255 | * Change labels in case type xml definition back to names. (dev/core#1046) | |
256 | * ONLY for ones using database storage - don't want to "fork" case types | |
257 | * that aren't currently forked. | |
258 | * | |
259 | * @param $isDryRun bool | |
260 | * If TRUE then don't actually change anything just report warnings. | |
261 | * | |
262 | * @return array List of warning messages. | |
263 | */ | |
264 | public static function _changeCaseTypeLabelToName($isDryRun = FALSE) { | |
265 | $messages = []; | |
266 | self::$relationshipTypes = civicrm_api3('RelationshipType', 'get', [ | |
267 | 'options' => ['limit' => 0], | |
268 | ])['values']; | |
269 | ||
270 | // Get all case types definitions that are using db storage | |
271 | $dao = CRM_Core_DAO::executeQuery("SELECT id FROM civicrm_case_type WHERE definition IS NOT NULL AND definition <> ''"); | |
272 | while ($dao->fetch()) { | |
273 | // array_merge so that existing numeric keys don't get overwritten | |
274 | $messages = array_merge($messages, self::_processCaseTypeLabelName($isDryRun, $dao->id)); | |
275 | } | |
276 | return $messages; | |
277 | } | |
278 | ||
279 | /** | |
280 | * Process a single case type for _changeCaseTypeLabelToName() | |
281 | * | |
282 | * @param $isDryRun bool | |
283 | * If TRUE then don't actually change anything just report warnings. | |
284 | * @param $caseTypeId int | |
285 | */ | |
286 | private static function _processCaseTypeLabelName($isDryRun, $caseTypeId) { | |
287 | $messages = []; | |
288 | $isDirty = FALSE; | |
289 | ||
290 | // Get the case type definition | |
291 | $caseType = civicrm_api3( | |
292 | 'CaseType', | |
293 | 'get', | |
294 | ['id' => $caseTypeId] | |
295 | )['values'][$caseTypeId]; | |
296 | ||
297 | foreach ($caseType['definition']['caseRoles'] as $roleSequenceId => $role) { | |
298 | // First double-check that there is a unique match on label so we | |
299 | // don't get it wrong. | |
300 | // There's maybe a fancy way to do this with array_XXX functions but | |
301 | // need to take into account edge cases where bidirectional but name | |
302 | // is different, or where somehow two labels are the same across types, | |
303 | // so do old-fashioned loop. | |
304 | ||
305 | $cantConvertMessage = NULL; | |
306 | $foundName = NULL; | |
307 | foreach (self::$relationshipTypes as $relationshipType) { | |
308 | // does it match one of our existing labels | |
309 | if ($relationshipType['label_a_b'] === $role['name'] || $relationshipType['label_b_a'] === $role['name']) { | |
310 | // So either it's ambiguous, in which case exit loop with a message, | |
311 | // or we have the name, so exit loop with that. | |
312 | $cantConvertMessage = self::checkAmbiguous($relationshipType, $caseType['name'], $role['name']); | |
313 | if (empty($cantConvertMessage)) { | |
314 | // not ambiguous, so note the corresponding name for the direction | |
315 | $foundName = ($relationshipType['label_a_b'] === $role['name']) ? $relationshipType['name_a_b'] : $relationshipType['name_b_a']; | |
316 | } | |
317 | break; | |
318 | } | |
319 | } | |
320 | ||
321 | if (empty($foundName) && empty($cantConvertMessage)) { | |
322 | // It's possible we went through all relationship types and didn't | |
323 | // find any match, so don't change anything. | |
324 | $cantConvertMessage = ts("Case Type '%1', role '%2' doesn't seem to be a valid role. See the administration console status messages for more info.", [ | |
325 | 1 => htmlspecialchars($caseType['name']), | |
326 | 2 => htmlspecialchars($role['name']), | |
327 | ]); | |
328 | } | |
329 | // Only two possibilities now are we have a name, or we have a message. | |
330 | // So the if($foundName) is redundant, but seems clearer somehow. | |
331 | if ($foundName && empty($cantConvertMessage)) { | |
332 | // If name and label are the same don't need to update anything. | |
333 | if ($foundName !== $role['name']) { | |
334 | $caseType['definition']['caseRoles'][$roleSequenceId]['name'] = $foundName; | |
335 | $isDirty = TRUE; | |
336 | } | |
337 | } | |
338 | else { | |
339 | $messages[] = $cantConvertMessage; | |
340 | } | |
341 | ||
342 | // end looping thru all roles in definition | |
343 | } | |
344 | ||
345 | // If this is a dry run during preupgrade checks we can skip this and | |
346 | // just return any messages. | |
347 | // If for real, then update the case type and here if there's errors | |
348 | // we don't really have a choice but to stop the entire upgrade | |
349 | // completely. There's no way to just send back messages during a queue | |
350 | // run. But we can log a message to error log so that the user has a | |
351 | // little more specific info about which case type. | |
352 | if ($isDirty && !$isDryRun) { | |
353 | $exception = NULL; | |
354 | try { | |
355 | $api_result = civicrm_api3('CaseType', 'create', $caseType); | |
356 | } | |
357 | catch (Exception $e) { | |
358 | $exception = $e; | |
359 | $errorMessage = ts("Error updating case type '%1': %2", [ | |
360 | 1 => htmlspecialchars($caseType['name']), | |
361 | 2 => htmlspecialchars($e->getMessage()), | |
362 | ]); | |
363 | CRM_Core_Error::debug_log_message($errorMessage); | |
364 | } | |
365 | if (!empty($api_result['is_error'])) { | |
366 | $errorMessage = ts("Error updating case type '%1': %2", [ | |
367 | 1 => htmlspecialchars($caseType['name']), | |
368 | 2 => htmlspecialchars($api_result['error_message']), | |
369 | ]); | |
370 | CRM_Core_Error::debug_log_message($errorMessage); | |
371 | $exception = new Exception($errorMessage); | |
372 | } | |
373 | // We need to rethrow the error which unfortunately stops the | |
374 | // entire upgrade including any further tasks. But otherwise | |
375 | // the only way to notify the user something went wrong is with a | |
376 | // crazy workaround. | |
377 | if ($exception) { | |
378 | throw $exception; | |
379 | } | |
380 | } | |
381 | ||
382 | return $messages; | |
383 | } | |
384 | ||
385 | /** | |
386 | * Helper for _processCaseTypeLabelName to check if a label can't be | |
387 | * converted unambiguously to name. | |
388 | * | |
389 | * If it's bidirectional, we can't convert it if there's an edge case | |
390 | * where the two names are different. | |
391 | * | |
392 | * If it's unidirectional, we can't convert it if there's an edge case | |
393 | * where there's another type that has the same label. | |
394 | * | |
395 | * @param $relationshipType array | |
396 | * @param $caseTypeName string | |
397 | * @param $xmlRoleName string | |
398 | * | |
399 | * @return string|NULL | |
400 | */ | |
401 | private static function checkAmbiguous($relationshipType, $caseTypeName, $xmlRoleName) { | |
402 | $cantConvertMessage = NULL; | |
403 | if ($relationshipType['label_a_b'] === $relationshipType['label_b_a']) { | |
404 | // bidirectional, so check if names are different for some reason | |
405 | if ($relationshipType['name_a_b'] !== $relationshipType['name_b_a']) { | |
406 | $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.", [ | |
407 | 1 => htmlspecialchars($caseTypeName), | |
408 | 2 => htmlspecialchars($xmlRoleName), | |
409 | ]); | |
410 | } | |
411 | } | |
412 | else { | |
413 | // Check if it matches either label_a_b or label_b_a for another type | |
414 | foreach (self::$relationshipTypes as $innerLoopId => $innerLoopType) { | |
415 | if ($innerLoopId == $relationshipType['id']) { | |
416 | // Only check types that aren't the same one we're on. | |
417 | // Sidenote: The loop index is integer but the 'id' member is string | |
418 | continue; | |
419 | } | |
420 | if ($innerLoopType['label_a_b'] === $xmlRoleName || $innerLoopType['label_b_a'] === $xmlRoleName) { | |
421 | $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.", [ | |
422 | 1 => htmlspecialchars($caseTypeName), | |
423 | 2 => htmlspecialchars($xmlRoleName), | |
424 | ]); | |
425 | break; | |
426 | } | |
427 | } | |
428 | } | |
429 | return $cantConvertMessage; | |
430 | } | |
431 | ||
187fc9e9 | 432 | } |