Merge pull request #15850 from civicrm/5.20
[civicrm-core.git] / CRM / Case / BAO / CaseType.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 *
14 * @package CRM
15 * @copyright CiviCRM LLC https://civicrm.org/licensing
16 */
17
18 /**
19 * This class contains the functions for Case Type management.
20 */
21 class CRM_Case_BAO_CaseType extends CRM_Case_DAO_CaseType {
22
23 /**
24 * Static field for all the case information that we can potentially export.
25 *
26 * @var array
27 */
28 public static $_exportableFields = NULL;
29
30 /**
31 * Takes an associative array and creates a Case Type object.
32 *
33 * the function extract all the params it needs to initialize the create a
34 * case type object. the params array could contain additional unused name/value
35 * pairs
36 *
37 * @param array $params
38 * (reference ) an assoc array of name/value pairs.
39 *
40 * @throws CRM_Core_Exception
41 *
42 * @return CRM_Case_BAO_CaseType
43 */
44 public static function add(&$params) {
45 $caseTypeDAO = new CRM_Case_DAO_CaseType();
46
47 // form the name only if missing: CRM-627
48 $nameParam = CRM_Utils_Array::value('name', $params, NULL);
49 if (!$nameParam && empty($params['id'])) {
50 $params['name'] = CRM_Utils_String::titleToVar($params['title']);
51 }
52
53 // Old case-types (pre-4.5) may keep their ucky names, but new case-types must satisfy isValidName()
54 if (empty($params['id']) && !empty($params['name']) && !CRM_Case_BAO_CaseType::isValidName($params['name'])) {
55 throw new CRM_Core_Exception("Cannot create new case-type with malformed name [{$params['name']}]");
56 }
57
58 $caseTypeName = (isset($params['name'])) ? $params['name'] : CRM_Core_DAO::getFieldValue('CRM_Case_DAO_CaseType', $params['id'], 'name', 'id', TRUE);
59
60 // function to format definition column
61 if (isset($params['definition']) && is_array($params['definition'])) {
62 $params['definition'] = self::convertDefinitionToXML($caseTypeName, $params['definition']);
63 CRM_Core_ManagedEntities::scheduleReconciliation();
64 }
65
66 $caseTypeDAO->copyValues($params);
67 $result = $caseTypeDAO->save();
68 CRM_Case_XMLRepository::singleton()->flush();
69 return $result;
70 }
71
72 /**
73 * Generate and assign an arbitrary value to a field of a test object.
74 *
75 * @param string $fieldName
76 * @param array $fieldDef
77 * @param int $counter
78 * The globally-unique ID of the test object.
79 */
80 protected function assignTestValue($fieldName, &$fieldDef, $counter) {
81 if ($fieldName == 'definition') {
82 $this->{$fieldName} = "<CaseType><name>TestCaseType{$counter}</name></CaseType>";
83 }
84 else {
85 parent::assignTestValue($fieldName, $fieldDef, $counter);
86 }
87 }
88
89 /**
90 * Format / convert submitted array to xml for case type definition
91 *
92 * @param string $name
93 * @param array $definition
94 * The case-type definition expressed as an array-tree.
95 * @return string
96 * XML
97 */
98 public static function convertDefinitionToXML($name, $definition) {
99 $xmlFile = '<?xml version="1.0" encoding="utf-8" ?>' . "\n\n<CaseType>\n";
100 $xmlFile .= "<name>" . self::encodeXmlString($name) . "</name>\n";
101
102 if (array_key_exists('forkable', $definition)) {
103 $xmlFile .= "<forkable>" . ((int) $definition['forkable']) . "</forkable>\n";
104 }
105
106 if (isset($definition['activityTypes'])) {
107 $xmlFile .= "<ActivityTypes>\n";
108 foreach ($definition['activityTypes'] as $values) {
109 $xmlFile .= "<ActivityType>\n";
110 foreach ($values as $key => $value) {
111 $xmlFile .= "<{$key}>" . self::encodeXmlString($value) . "</{$key}>\n";
112 }
113 $xmlFile .= "</ActivityType>\n";
114 }
115 $xmlFile .= "</ActivityTypes>\n";
116 }
117
118 if (!empty($definition['statuses'])) {
119 $xmlFile .= "<Statuses>\n";
120 foreach ($definition['statuses'] as $value) {
121 $xmlFile .= "<Status>$value</Status>\n";
122 }
123 $xmlFile .= "</Statuses>\n";
124 }
125
126 if (isset($definition['activitySets'])) {
127 $xmlFile .= "<ActivitySets>\n";
128 foreach ($definition['activitySets'] as $k => $val) {
129 $xmlFile .= "<ActivitySet>\n";
130 foreach ($val as $index => $setVal) {
131 switch ($index) {
132 case 'activityTypes':
133 if (!empty($setVal)) {
134 $xmlFile .= "<ActivityTypes>\n";
135 foreach ($setVal as $values) {
136 $xmlFile .= "<ActivityType>\n";
137 foreach ($values as $key => $value) {
138 $xmlFile .= "<{$key}>" . self::encodeXmlString($value) . "</{$key}>\n";
139 }
140 $xmlFile .= "</ActivityType>\n";
141 }
142 $xmlFile .= "</ActivityTypes>\n";
143 }
144 break;
145
146 // passthrough
147 case 'sequence':
148 case 'timeline':
149 if ($setVal) {
150 $xmlFile .= "<{$index}>true</{$index}>\n";
151 }
152 break;
153
154 default:
155 $xmlFile .= "<{$index}>" . self::encodeXmlString($setVal) . "</{$index}>\n";
156 }
157 }
158
159 $xmlFile .= "</ActivitySet>\n";
160 }
161
162 $xmlFile .= "</ActivitySets>\n";
163 }
164
165 if (isset($definition['caseRoles'])) {
166 $xmlFile .= "<CaseRoles>\n";
167 foreach ($definition['caseRoles'] as $values) {
168 $xmlFile .= "<RelationshipType>\n";
169 foreach ($values as $key => $value) {
170 $xmlFile .= "<{$key}>" . self::encodeXmlString($value) . "</{$key}>\n";
171 }
172 $xmlFile .= "</RelationshipType>\n";
173 }
174 $xmlFile .= "</CaseRoles>\n";
175 }
176
177 if (array_key_exists('restrictActivityAsgmtToCmsUser', $definition)) {
178 $xmlFile .= "<RestrictActivityAsgmtToCmsUser>" . $definition['restrictActivityAsgmtToCmsUser'] . "</RestrictActivityAsgmtToCmsUser>\n";
179 }
180
181 if (!empty($definition['activityAsgmtGrps'])) {
182 $xmlFile .= "<ActivityAsgmtGrps>\n";
183 foreach ($definition['activityAsgmtGrps'] as $value) {
184 $xmlFile .= "<Group>$value</Group>\n";
185 }
186 $xmlFile .= "</ActivityAsgmtGrps>\n";
187 }
188
189 $xmlFile .= '</CaseType>';
190
191 return $xmlFile;
192 }
193
194 /**
195 * Ugh. This shouldn't exist. Use a real XML-encoder.
196 *
197 * Escape a string for use in XML.
198 *
199 * @param string $str
200 * A string which should outputted to XML.
201 * @return string
202 * @deprecated
203 */
204 protected static function encodeXmlString($str) {
205 // PHP 5.4: return htmlspecialchars($str, ENT_XML1, 'UTF-8')
206 if (is_scalar($str)) {
207 return htmlspecialchars($str);
208 }
209 else {
210 return NULL;
211 }
212 }
213
214 /**
215 * Get the case definition either from db or read from xml file.
216 *
217 * @param SimpleXmlElement $xml
218 * A single case-type record.
219 *
220 * @return array
221 * the definition of the case-type, expressed as PHP array-tree
222 */
223 public static function convertXmlToDefinition($xml) {
224 // build PHP array based on definition
225 $definition = [];
226
227 if (isset($xml->forkable)) {
228 $definition['forkable'] = (int) $xml->forkable;
229 }
230
231 if (isset($xml->RestrictActivityAsgmtToCmsUser)) {
232 $definition['restrictActivityAsgmtToCmsUser'] = (int) $xml->RestrictActivityAsgmtToCmsUser;
233 }
234
235 if (isset($xml->ActivityAsgmtGrps)) {
236 $definition['activityAsgmtGrps'] = (array) $xml->ActivityAsgmtGrps->Group;
237 }
238
239 // set activity types
240 if (isset($xml->ActivityTypes)) {
241 $definition['activityTypes'] = [];
242 foreach ($xml->ActivityTypes->ActivityType as $activityTypeXML) {
243 $definition['activityTypes'][] = json_decode(json_encode($activityTypeXML), TRUE);
244 }
245 }
246
247 // set statuses
248 if (isset($xml->Statuses)) {
249 $definition['statuses'] = (array) $xml->Statuses->Status;
250 }
251
252 // set activity sets
253 if (isset($xml->ActivitySets)) {
254 $definition['activitySets'] = [];
255 $definition['timelineActivityTypes'] = [];
256
257 foreach ($xml->ActivitySets->ActivitySet as $activitySetXML) {
258 // parse basic properties
259 $activitySet = [];
260 $activitySet['name'] = (string) $activitySetXML->name;
261 $activitySet['label'] = (string) $activitySetXML->label;
262 if ('true' == (string) $activitySetXML->timeline) {
263 $activitySet['timeline'] = 1;
264 }
265 if ('true' == (string) $activitySetXML->sequence) {
266 $activitySet['sequence'] = 1;
267 }
268
269 if (isset($activitySetXML->ActivityTypes)) {
270 $activitySet['activityTypes'] = [];
271 foreach ($activitySetXML->ActivityTypes->ActivityType as $activityTypeXML) {
272 $activityType = json_decode(json_encode($activityTypeXML), TRUE);
273 $activitySet['activityTypes'][] = $activityType;
274 if ($activitySetXML->timeline) {
275 $definition['timelineActivityTypes'][] = $activityType;
276 }
277 }
278 }
279 $definition['activitySets'][] = $activitySet;
280 }
281 }
282
283 // set case roles
284 if (isset($xml->CaseRoles)) {
285 $definition['caseRoles'] = [];
286 foreach ($xml->CaseRoles->RelationshipType as $caseRoleXml) {
287 $definition['caseRoles'][] = json_decode(json_encode($caseRoleXml), TRUE);
288 }
289 }
290
291 return $definition;
292 }
293
294 /**
295 * Given the list of params in the params array, fetch the object
296 * and store the values in the values array
297 *
298 * @param array $params
299 * Input parameters to find object.
300 * @param array $values
301 * Output values of the object.
302 *
303 * @return CRM_Case_BAO_CaseType|null the found object or null
304 */
305 public static function &getValues(&$params, &$values) {
306 $caseType = new CRM_Case_BAO_CaseType();
307
308 $caseType->copyValues($params);
309
310 if ($caseType->find(TRUE)) {
311 CRM_Core_DAO::storeValues($caseType, $values);
312 return $caseType;
313 }
314 return NULL;
315 }
316
317 /**
318 * Takes an associative array and creates a case type object.
319 *
320 * @param array $params
321 * (reference ) an assoc array of name/value pairs.
322 *
323 * @return CRM_Case_BAO_CaseType
324 */
325 public static function &create(&$params) {
326 $transaction = new CRM_Core_Transaction();
327
328 if (!empty($params['id'])) {
329 CRM_Utils_Hook::pre('edit', 'CaseType', $params['id'], $params);
330 }
331 else {
332 CRM_Utils_Hook::pre('create', 'CaseType', NULL, $params);
333 }
334
335 $caseType = self::add($params);
336
337 if (is_a($caseType, 'CRM_Core_Error')) {
338 $transaction->rollback();
339 return $caseType;
340 }
341
342 if (!empty($params['id'])) {
343 CRM_Utils_Hook::post('edit', 'CaseType', $caseType->id, $case);
344 }
345 else {
346 CRM_Utils_Hook::post('create', 'CaseType', $caseType->id, $case);
347 }
348 $transaction->commit();
349 CRM_Case_XMLRepository::singleton(TRUE);
350 CRM_Core_OptionGroup::flushAll();
351
352 return $caseType;
353 }
354
355 /**
356 * Retrieve DB object based on input parameters.
357 *
358 * It also stores all the retrieved values in the default array.
359 *
360 * @param array $params
361 * (reference ) an assoc array of name/value pairs.
362 * @param array $defaults
363 * (reference ) an assoc array to hold the name / value pairs.
364 * in a hierarchical manner
365 *
366 * @return CRM_Case_BAO_CaseType
367 */
368 public static function retrieve(&$params, &$defaults) {
369 $caseType = CRM_Case_BAO_CaseType::getValues($params, $defaults);
370 return $caseType;
371 }
372
373 /**
374 * @param int $caseTypeId
375 *
376 * @throws CRM_Core_Exception
377 * @return mixed
378 */
379 public static function del($caseTypeId) {
380 $caseType = new CRM_Case_DAO_CaseType();
381 $caseType->id = $caseTypeId;
382 $refCounts = $caseType->getReferenceCounts();
383 $total = array_sum(CRM_Utils_Array::collect('count', $refCounts));
384 if ($total) {
385 throw new CRM_Core_Exception(ts("You can not delete this case type -- it is assigned to %1 existing case record(s). If you do not want this case type to be used going forward, consider disabling it instead.", [1 => $total]));
386 }
387 $result = $caseType->delete();
388 CRM_Case_XMLRepository::singleton(TRUE);
389 return $result;
390 }
391
392 /**
393 * Determine if a case-type name is well-formed
394 *
395 * @param string $caseType
396 * @return bool
397 */
398 public static function isValidName($caseType) {
399 return preg_match('/^[a-zA-Z0-9_]+$/', $caseType);
400 }
401
402 /**
403 * Determine if the case-type has *both* DB and file-based definitions.
404 *
405 * @param int $caseTypeId
406 * @return bool|null
407 * TRUE if there are *both* DB and file-based definitions
408 */
409 public static function isForked($caseTypeId) {
410 $caseTypeName = CRM_Core_DAO::getFieldValue('CRM_Case_DAO_CaseType', $caseTypeId, 'name', 'id', TRUE);
411 if ($caseTypeName) {
412 $dbDefinition = CRM_Core_DAO::getFieldValue('CRM_Case_DAO_CaseType', $caseTypeId, 'definition', 'id', TRUE);
413 $fileDefinition = CRM_Case_XMLRepository::singleton()->retrieveFile($caseTypeName);
414 return $fileDefinition && $dbDefinition;
415 }
416 return NULL;
417 }
418
419 /**
420 * Determine if modifications are allowed on the case-type
421 *
422 * @param int $caseTypeId
423 * @return bool
424 * TRUE if the definition can be modified
425 */
426 public static function isForkable($caseTypeId) {
427 $caseTypeName = CRM_Core_DAO::getFieldValue('CRM_Case_DAO_CaseType', $caseTypeId, 'name', 'id', TRUE);
428 if ($caseTypeName) {
429 // if file-based definition explicitly disables "forkable" option, then don't allow changes to definition
430 $fileDefinition = CRM_Case_XMLRepository::singleton()->retrieveFile($caseTypeName);
431 if ($fileDefinition && isset($fileDefinition->forkable)) {
432 return CRM_Utils_String::strtobool((string) $fileDefinition->forkable);
433 }
434 }
435 return TRUE;
436 }
437
438 }