Various phpdoc fixes
[civicrm-core.git] / CRM / Upgrade / Incremental / php / FiveTwenty.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
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 |
9 +--------------------------------------------------------------------+
10 */
11
12 /**
13 * Upgrade logic for FiveTwenty
14 */
15 class CRM_Upgrade_Incremental_php_FiveTwenty extends CRM_Upgrade_Incremental_Base {
16
17 /**
18 * @var array
19 * api call result keyed on relationship_type.id
20 */
21 protected static $relationshipTypes;
22
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) {
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 }
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 }
50 }
51 }
52
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');
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');
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');
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');
68 $this->addTask('Change labels back to names in case type xml', 'changeCaseTypeLabelToName');
69 }
70 $this->addTask(ts('Upgrade DB to %1: SQL', [1 => $rev]), 'runSql', $rev);
71 $this->addTask('Add "Template" contribution status', 'templateStatus');
72 $this->addTask('Clean up unused table "civicrm_persistent"', 'dropTableIfEmpty', 'civicrm_persistent');
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;
84 }
85
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 *
121 * @param int $caseTypeId
122 * @param string $definition
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 *
163 * @param int $relationshipTypeId
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
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 *
193 * @param bool $isDryRun
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 *
216 * @param bool $isDryRun
217 * If TRUE then don't actually change anything just report warnings.
218 * @param int $caseTypeId
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 *
329 * @param array $relationshipType
330 * @param string $caseTypeName
331 * @param string $xmlRoleName
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
366 }