3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
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 +--------------------------------------------------------------------+
15 * @copyright CiviCRM LLC https://civicrm.org/licensing
17 class CRM_Utils_Check_Component_Case
extends CRM_Utils_Check_Component
{
19 const DOCTOR_WHEN
= 'https://github.com/civicrm/org.civicrm.doctorwhen';
22 * @var CRM_Case_XMLRepository
29 protected $caseTypeNames;
34 public function __construct() {
35 $this->caseTypeNames
= CRM_Case_PseudoConstant
::caseType('name');
36 $this->xmlRepo
= CRM_Case_XMLRepository
::singleton();
42 public function isEnabled() {
43 return CRM_Case_BAO_Case
::enabled();
47 * Check that the case-type names don't rely on double-munging.
49 * @return array<CRM_Utils_Check_Message>
50 * An empty array, or a list of warnings
52 public function checkCaseTypeNameConsistency() {
55 foreach ($this->caseTypeNames
as $caseTypeName) {
56 $normalFile = $this->xmlRepo
->findXmlFile($caseTypeName);
57 $mungedFile = $this->xmlRepo
->findXmlFile(CRM_Case_XMLProcessor
::mungeCaseType($caseTypeName));
59 if ($normalFile && $mungedFile && $normalFile == $mungedFile) {
62 elseif ($normalFile && $mungedFile) {
63 $messages[] = new CRM_Utils_Check_Message(
64 __FUNCTION__
. $caseTypeName,
65 ts('Case type "%1" has duplicate XML files ("%2" and "%3")', [
70 '<br /><a href="' . CRM_Utils_System
::getWikiBaseURL() . __FUNCTION__
. '">' .
71 ts('Read more about this warning') .
74 \Psr\Log\LogLevel
::WARNING
,
78 elseif ($normalFile && !$mungedFile) {
81 elseif (!$normalFile && $mungedFile) {
82 $messages[] = new CRM_Utils_Check_Message(
83 __FUNCTION__
. $caseTypeName,
84 ts('Case type "%1" corresponds to XML file ("%2") The XML file should be named "%3".', [
87 3 => "{$caseTypeName}.xml",
89 '<br /><a href="' . CRM_Utils_System
::getWikiBaseURL() . __FUNCTION__
. '">' .
90 ts('Read more about this warning') .
93 \Psr\Log\LogLevel
::WARNING
,
97 elseif (!$normalFile && !$mungedFile) {
98 // ok -- probably a new or DB-based CaseType
106 * Check that the timestamp columns are populated. (CRM-20958)
108 * @return array<CRM_Utils_Check_Message>
109 * An empty array, or a list of warnings
111 public function checkNullTimestamps() {
115 $nullCount +
= CRM_Utils_SQL_Select
::from('civicrm_activity')
116 ->where('created_date IS NULL OR modified_date IS NULL')
120 $nullCount +
= CRM_Utils_SQL_Select
::from('civicrm_case')
121 ->where('created_date IS NULL OR modified_date IS NULL')
126 if ($nullCount > 0) {
127 $messages[] = new CRM_Utils_Check_Message(
130 ts('The tables "<em>civicrm_activity</em>" and "<em>civicrm_case</em>" were updated to support two new fields, "<em>created_date</em>" and "<em>modified_date</em>". For historical data, these fields may appear blank. (%1 records have NULL timestamps.)', [
134 ts('At time of writing, this is not a problem. However, future extensions and improvements could rely on these fields, so it may be useful to back-fill them.') .
136 ts('For further discussion, please visit %1', [
137 1 => sprintf('<a href="%s" target="_blank">%s</a>', self
::DOCTOR_WHEN
, self
::DOCTOR_WHEN
),
140 ts('Timestamps for Activities and Cases'),
141 \Psr\Log\LogLevel
::NOTICE
,
150 * Check that the relationship types aren't going to cause problems.
152 * @return array<CRM_Utils_Check_Message>
153 * An empty array, or a list of warnings
155 public function checkRelationshipTypeProblems() {
159 * There's no use-case to have two different relationship types
160 * with the same machine name, and it will cause problems because the
161 * system might match up the wrong type when comparing to xml.
162 * A single bi-directional one CAN and probably does have the same
163 * name_a_b and name_b_a and that's ok.
166 $dao = CRM_Core_DAO
::executeQuery("SELECT rt1.*, rt2.id AS id2, rt2.name_a_b AS nameab2, rt2.name_b_a AS nameba2 FROM civicrm_relationship_type rt1 INNER JOIN civicrm_relationship_type rt2 ON (rt1.name_a_b = rt2.name_a_b OR rt1.name_a_b = rt2.name_b_a) WHERE rt1.id <> rt2.id");
167 while ($dao->fetch()) {
168 $messages[] = new CRM_Utils_Check_Message(
169 __FUNCTION__
. $dao->id
. "dupe1",
170 ts("Relationship type <em>%1</em> has the same internal machine name as another type.
172 <tr><th>ID</th><th>name_a_b</th><th>name_b_a</th></tr>
173 <tr><td>%2</td><td>%3</td><td>%4</td></tr>
174 <tr><td>%5</td><td>%6</td><td>%7</td></tr>
176 1 => htmlspecialchars($dao->label_a_b
),
178 3 => htmlspecialchars($dao->name_a_b
),
179 4 => htmlspecialchars($dao->name_b_a
),
181 6 => htmlspecialchars($dao->nameab2
),
182 7 => htmlspecialchars($dao->nameba2
),
184 '<br /><a href="' . CRM_Utils_System
::docURL2('user/case-management/what-you-need-to-know#relationship-type-internal-name-duplicates', TRUE) . '">' .
185 ts('Read more about this warning') .
187 ts('Relationship Type Internal Name Duplicates'),
188 \Psr\Log\LogLevel
::ERROR
,
194 $dao = CRM_Core_DAO
::executeQuery("SELECT rt1.*, rt2.id AS id2, rt2.label_a_b AS labelab2, rt2.label_b_a AS labelba2 FROM civicrm_relationship_type rt1 INNER JOIN civicrm_relationship_type rt2 ON (rt1.label_a_b = rt2.label_a_b OR rt1.label_a_b = rt2.label_b_a) WHERE rt1.id <> rt2.id");
195 while ($dao->fetch()) {
196 $messages[] = new CRM_Utils_Check_Message(
197 __FUNCTION__
. $dao->id
. "dupe2",
198 ts("Relationship type <em>%1</em> has the same display label as another type.
200 <tr><th>ID</th><th>label_a_b</th><th>label_b_a</th></tr>
201 <tr><td>%2</td><td>%3</td><td>%4</td></tr>
202 <tr><td>%5</td><td>%6</td><td>%7</td></tr>
204 1 => htmlspecialchars($dao->label_a_b
),
206 3 => htmlspecialchars($dao->label_a_b
),
207 4 => htmlspecialchars($dao->label_b_a
),
209 6 => htmlspecialchars($dao->labelab2
),
210 7 => htmlspecialchars($dao->labelba2
),
212 '<br /><a href="' . CRM_Utils_System
::docURL2('user/case-management/what-you-need-to-know#relationship-type-display-label-duplicates', TRUE) . '">' .
213 ts('Read more about this warning') .
215 ts('Relationship Type Display Label Duplicates'),
216 \Psr\Log\LogLevel
::ERROR
,
222 * If the name of one type matches the label of another type, there may
223 * also be problems. This can happen if for example you initially set
224 * it up and then keep changing your mind adding and deleting and renaming
225 * a couple times in a certain order.
227 $dao = CRM_Core_DAO
::executeQuery("SELECT rt1.*, rt2.id AS id2, rt2.name_a_b AS nameab2, rt2.name_b_a AS nameba2, rt2.label_a_b AS labelab2, rt2.label_b_a AS labelba2 FROM civicrm_relationship_type rt1 INNER JOIN civicrm_relationship_type rt2 ON (rt1.name_a_b = rt2.label_a_b OR rt1.name_b_a = rt2.label_a_b OR rt1.name_a_b = rt2.label_b_a OR rt1.name_b_a = rt2.label_b_a) WHERE rt1.id <> rt2.id");
228 // No point displaying the same matching id twice, which can happen with
231 while ($dao->fetch()) {
232 if (isset($ids[$dao->id2
])) {
235 $ids[$dao->id
] = $dao->id
;
236 $messages[] = new CRM_Utils_Check_Message(
237 __FUNCTION__
. $dao->id
. "dupe3",
238 ts("Relationship type <em>%1</em> has an internal machine name that is the same as the display label as another type.
240 <tr><th>ID</th><th>name_a_b</th><th>name_b_a</th><th>label_a_b</th><th>label_b_a</th></tr>
241 <tr><td>%2</td><td>%3</td><td>%4</td><td>%5</td><td>%6</td></tr>
242 <tr><td>%7</td><td>%8</td><td>%9</td><td>%10</td><td>%11</td></tr>
244 1 => htmlspecialchars($dao->label_a_b
),
246 3 => htmlspecialchars($dao->name_a_b
),
247 4 => htmlspecialchars($dao->name_b_a
),
248 5 => htmlspecialchars($dao->label_a_b
),
249 6 => htmlspecialchars($dao->label_b_a
),
251 8 => htmlspecialchars($dao->nameab2
),
252 9 => htmlspecialchars($dao->nameab2
),
253 10 => htmlspecialchars($dao->labelab2
),
254 11 => htmlspecialchars($dao->labelba2
),
256 '<br /><a href="' . CRM_Utils_System
::docURL2('user/case-management/what-you-need-to-know#relationship-type-cross-duplication', TRUE) . '">' .
257 ts('Read more about this warning') .
259 ts('Relationship Type Cross-Duplication'),
260 \Psr\Log\LogLevel
::WARNING
,
266 * Check that ones that appear to be unidirectional don't have the same
267 * machine name for both a_b and b_a. This can happen for example if you
268 * forget to fill in the b_a label when creating, then go back and edit.
270 $dao = CRM_Core_DAO
::executeQuery("SELECT rt1.* FROM civicrm_relationship_type rt1 WHERE rt1.name_a_b = rt1.name_b_a AND rt1.label_a_b <> rt1.label_b_a");
271 while ($dao->fetch()) {
272 $messages[] = new CRM_Utils_Check_Message(
273 __FUNCTION__
. $dao->id
. "ambiguousname",
274 ts("Relationship type <em>%1</em> appears to be unidirectional, but has the same internal machine name for both sides.
276 <tr><th>ID</th><th>name_a_b</th><th>name_b_a</th><th>label_a_b</th><th>label_b_a</th></tr>
277 <tr><td>%2</td><td>%3</td><td>%4</td><td>%5</td><td>%6</td></tr>
279 1 => htmlspecialchars($dao->label_a_b
),
281 3 => htmlspecialchars($dao->name_a_b
),
282 4 => htmlspecialchars($dao->name_b_a
),
283 5 => htmlspecialchars($dao->label_a_b
),
284 6 => htmlspecialchars($dao->label_b_a
),
286 '<br /><a href="' . CRM_Utils_System
::docURL2('user/case-management/what-you-need-to-know#relationship-type-ambiguity', TRUE) . '">' .
287 ts('Read more about this warning') .
289 ts('Relationship Type Ambiguity'),
290 \Psr\Log\LogLevel
::WARNING
,
296 * Check that ones that appear to be unidirectional don't have the same
297 * label for both a_b and b_a. This can happen for example if you
298 * created it as unidirectional, then edited it later trying to make it
301 $dao = CRM_Core_DAO
::executeQuery("SELECT rt1.* FROM civicrm_relationship_type rt1 WHERE rt1.label_a_b = rt1.label_b_a AND rt1.name_a_b <> rt1.name_b_a");
302 while ($dao->fetch()) {
303 $messages[] = new CRM_Utils_Check_Message(
304 __FUNCTION__
. $dao->id
. "ambiguouslabel",
305 ts("Relationship type <em>%1</em> appears to be unidirectional internally, but has the same display label for both sides. Possibly you created it initially as unidirectional and then made it bidirectional later.
307 <tr><th>ID</th><th>name_a_b</th><th>name_b_a</th><th>label_a_b</th><th>label_b_a</th></tr>
308 <tr><td>%2</td><td>%3</td><td>%4</td><td>%5</td><td>%6</td></tr>
310 1 => htmlspecialchars($dao->label_a_b
),
312 3 => htmlspecialchars($dao->name_a_b
),
313 4 => htmlspecialchars($dao->name_b_a
),
314 5 => htmlspecialchars($dao->label_a_b
),
315 6 => htmlspecialchars($dao->label_b_a
),
317 '<br /><a href="' . CRM_Utils_System
::docURL2('user/case-management/what-you-need-to-know#relationship-type-ambiguity', TRUE) . '">' .
318 ts('Read more about this warning') .
320 ts('Relationship Type Ambiguity'),
321 \Psr\Log\LogLevel
::WARNING
,
327 * Check for missing roles listed in the xml but not defined as
328 * relationship types.
331 // Don't use database since might be in xml files.
332 $caseTypes = civicrm_api3('CaseType', 'get', [
333 'options' => ['limit' => 0],
335 // Don't use pseudoconstant since want all and also name and label.
336 $relationshipTypes = civicrm_api3('RelationshipType', 'get', [
337 'options' => ['limit' => 0],
339 $allConfigured = array_column($relationshipTypes, 'id', 'name_a_b')
340 +
array_column($relationshipTypes, 'id', 'name_b_a')
341 +
array_column($relationshipTypes, 'id', 'label_a_b')
342 +
array_column($relationshipTypes, 'id', 'label_b_a');
344 foreach ($caseTypes as $caseType) {
345 foreach ($caseType['definition']['caseRoles'] as $role) {
346 if (!isset($allConfigured[$role['name']])) {
347 $missing[$role['name']] = $role['name'];
351 if (!empty($missing)) {
353 foreach ($relationshipTypes as $relationshipType) {
354 $tableRows[] = ts('<tr><td>%1</td><td>%2</td><td>%3</td><td>%4</td><td>%5</td></tr>', [
355 1 => $relationshipType['id'],
356 2 => htmlspecialchars($relationshipType['name_a_b']),
357 3 => htmlspecialchars($relationshipType['name_b_a']),
358 4 => htmlspecialchars($relationshipType['label_a_b']),
359 5 => htmlspecialchars($relationshipType['label_b_a']),
362 $messages[] = new CRM_Utils_Check_Message(
363 __FUNCTION__
. "missingroles",
364 ts("<p>The following roles listed in your case type definitions do not match any relationship type defined in the system: <em>%1</em>.</p>"
365 . "<p>This might be because of a mismatch if you are using external xml files to manage case types. If using xml files, then use either the name_a_b or name_b_a value from the following table. (Out of the box you would use name_b_a, which lists them on the case from the client perspective.) If you are not using xml files, you can edit your case types at Administer - CiviCase - Case Types.</p>"
367 <tr><th>ID</th><th>name_a_b</th><th>name_b_a</th><th>label_a_b</th><th>label_b_a</th></tr>"
368 . implode("\n", $tableRows)
370 1 => htmlspecialchars(implode(', ', $missing)),
372 '<br /><a href="' . CRM_Utils_System
::docURL2('user/case-management/what-you-need-to-know#missing-roles', TRUE) . '">' .
373 ts('Read more about this warning') .
376 \Psr\Log\LogLevel
::ERROR
,
385 * Check any xml definitions stored as external files to see if they
386 * have label as the role and where the label is different from the name.
387 * We don't have to think about edge cases because there are already
388 * status checks above for those.
390 * @return array<CRM_Utils_Check_Message>
391 * An empty array, or a list of warnings
393 public function checkExternalXmlFileRoleNames() {
396 // Get config for relationship types
397 $relationship_types = civicrm_api3('RelationshipType', 'get', [
398 'options' => ['limit' => 0],
400 // keyed on name, with id as the value, e.g. 'Case Coordinator is' => 10
401 $names_a_b = array_column($relationship_types, 'id', 'name_a_b');
402 $names_b_a = array_column($relationship_types, 'id', 'name_b_a');
403 $labels_a_b = array_column($relationship_types, 'id', 'label_a_b');
404 $labels_b_a = array_column($relationship_types, 'id', 'label_b_a');
406 $dao = CRM_Core_DAO
::executeQuery("SELECT id FROM civicrm_case_type WHERE definition IS NULL OR definition=''");
407 while ($dao->fetch()) {
408 $case_type = civicrm_api3('CaseType', 'get', [
410 ])['values'][$dao->id
];
411 if (empty($case_type['definition'])) {
412 $messages[] = new CRM_Utils_Check_Message(
413 __FUNCTION__
. "missingcasetypedefinition",
414 '<p>' . ts('Unable to locate xml file for Case Type "<em>%1</em>".',
416 1 => htmlspecialchars(empty($case_type['title']) ?
$dao->id
: $case_type['title']),
418 ts('Missing Case Type Definition'),
419 \Psr\Log\LogLevel
::ERROR
,
425 if (empty($case_type['definition']['caseRoles'])) {
426 $messages[] = new CRM_Utils_Check_Message(
427 __FUNCTION__
. "missingcaseroles",
428 '<p>' . ts('CaseRoles seems to be missing in the xml file for Case Type "<em>%1</em>".',
430 1 => htmlspecialchars(empty($case_type['title']) ?
$dao->id
: $case_type['title']),
432 ts('Missing Case Roles'),
433 \Psr\Log\LogLevel
::ERROR
,
439 // Loop thru each role in the xml.
440 foreach ($case_type['definition']['caseRoles'] as $role) {
441 $name_to_suggest = NULL;
442 $xml_name = $role['name'];
443 if (isset($names_a_b[$xml_name]) ||
isset($names_b_a[$xml_name])) {
444 // It matches a name, so either name and label are the same or it's
445 // an edge case already dealt with by core status checks, so do
449 elseif (isset($labels_b_a[$xml_name])) {
450 // $labels_b_a[$xml_name] gives us the id, so then look up name_b_a
451 // from the original relationship_types array which is keyed on id.
452 // We do b_a first because it's the more standard one, although it
453 // will only make a difference in edge cases which we leave to the
455 $name_to_suggest = $relationship_types[$labels_b_a[$xml_name]]['name_b_a'];
457 elseif (isset($labels_a_b[$xml_name])) {
458 $name_to_suggest = $relationship_types[$labels_a_b[$xml_name]]['name_a_b'];
461 // If it didn't match any name or label then that's weird.
462 if (empty($name_to_suggest)) {
463 $messages[] = new CRM_Utils_Check_Message(
464 __FUNCTION__
. "invalidcaserole",
465 '<p>' . ts('CaseRole "<em>%1</em>" in the xml file for Case Type "<em>%2</em>" doesn\'t seem to match any existing relationship type.',
467 1 => htmlspecialchars($xml_name),
468 2 => htmlspecialchars(empty($case_type['title']) ?
$dao->id
: $case_type['title']),
470 ts('Invalid Case Role'),
471 \Psr\Log\LogLevel
::ERROR
,
476 $messages[] = new CRM_Utils_Check_Message(
477 __FUNCTION__
. "suggestedchange",
478 '<p>' . ts('Please edit the XML file for case type "<em>%2</em>" so that the case role label "<em>%1</em>" is changed to its corresponding name "<em>%3</em>". Using label is deprecated as of version 5.20.',
480 1 => htmlspecialchars($xml_name),
481 2 => htmlspecialchars(empty($case_type['title']) ?
$dao->id
: $case_type['title']),
482 3 => htmlspecialchars($name_to_suggest),
484 ts('Case Role using display label instead of internal machine name'),
485 \Psr\Log\LogLevel
::WARNING
,