CRM-19346 Add in form rule to ensure that option values can't use the same value
[civicrm-core.git] / CRM / Admin / Form / Options.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.7 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2017 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
9 | |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
13 | |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
18 | |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
26 */
27
28 /**
29 *
30 * @package CRM
31 * @copyright CiviCRM LLC (c) 2004-2017
32 */
33
34 /**
35 * This class generates form components for Options.
36 */
37 class CRM_Admin_Form_Options extends CRM_Admin_Form {
38
39 /**
40 * The option group name.
41 *
42 * @var array
43 */
44 protected $_gName;
45
46 /**
47 * The option group name in display format (capitalized, without underscores...etc)
48 *
49 * @var array
50 */
51 protected $_gLabel;
52
53 /**
54 * Is this Option Group Domain Specific
55 * @var bool
56 */
57 protected $_domainSpecific = FALSE;
58
59 /**
60 * Pre-process
61 */
62 public function preProcess() {
63 parent::preProcess();
64 $session = CRM_Core_Session::singleton();
65 if (!$this->_gName && !empty($this->urlPath[3])) {
66 $this->_gName = $this->urlPath[3];
67 }
68 if (!$this->_gName && !empty($_GET['gid'])) {
69 $this->_gName = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_OptionGroup', (int) $_GET['gid'], 'name');
70 }
71 if ($this->_gName) {
72 $this->set('gName', $this->_gName);
73 }
74 else {
75 $this->_gName = $this->get('gName');
76 }
77 $this->_gid = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_OptionGroup',
78 $this->_gName,
79 'id',
80 'name'
81 );
82 $this->_gLabel = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_OptionGroup', $this->_gid, 'title');
83 $url = "civicrm/admin/options/{$this->_gName}";
84 $params = "reset=1";
85
86 if (($this->_action & CRM_Core_Action::DELETE) &&
87 in_array($this->_gName, array('email_greeting', 'postal_greeting', 'addressee'))
88 ) {
89 // Don't allow delete if the option value belongs to addressee, postal or email greetings and is in use.
90 $findValue = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_OptionValue', $this->_id, 'value');
91 $queryParam = array(1 => array($findValue, 'Integer'));
92 $columnName = $this->_gName . "_id";
93 $sql = "SELECT count(id) FROM civicrm_contact WHERE " . $columnName . " = %1";
94 $isInUse = CRM_Core_DAO::singleValueQuery($sql, $queryParam);
95 if ($isInUse) {
96 $scriptURL = "<a href='" . CRM_Utils_System::docURL2('Update Greetings and Address Data for Contacts', TRUE, NULL, NULL, NULL, "wiki") . "'>" . ts('Learn more about a script that can automatically update contact addressee and greeting options.') . "</a>";
97 CRM_Core_Session::setStatus(ts('The selected %1 option has <strong>not been deleted</strong> because it is currently in use. Please update these contacts to use a different format before deleting this option. %2', array(
98 1 => $this->_gLabel,
99 2 => $scriptURL,
100 )), ts('Sorry'), 'error');
101 $redirect = CRM_Utils_System::url($url, $params);
102 CRM_Utils_System::redirect($redirect);
103 }
104 }
105
106 $session->pushUserContext(CRM_Utils_System::url($url, $params));
107 $this->assign('id', $this->_id);
108
109 if ($this->_id && in_array($this->_gName, CRM_Core_OptionGroup::$_domainIDGroups)) {
110 $domainID = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_OptionValue', $this->_id, 'domain_id', 'id');
111 if (CRM_Core_Config::domainID() != $domainID) {
112 CRM_Core_Error::fatal(ts('You do not have permission to access this page.'));
113 }
114 }
115 }
116
117 /**
118 * Set default values for the form.
119 */
120 public function setDefaultValues() {
121 $defaults = parent::setDefaultValues();
122
123 // Default weight & value
124 $fieldValues = array('option_group_id' => $this->_gid);
125 foreach (array('weight', 'value') as $field) {
126 if (empty($defaults[$field])) {
127 $defaults[$field] = CRM_Utils_Weight::getDefaultWeight('CRM_Core_DAO_OptionValue', $fieldValues, $field);
128 }
129 }
130
131 //setDefault of contact types for email greeting, postal greeting, addressee, CRM-4575
132 if (in_array($this->_gName, array(
133 'email_greeting',
134 'postal_greeting',
135 'addressee',
136 ))) {
137 $defaults['contactOptions'] = (CRM_Utils_Array::value('filter', $defaults)) ? $defaults['filter'] : NULL;
138 }
139 // CRM-11516
140 if ($this->_gName == 'payment_instrument' && $this->_id) {
141 $defaults['financial_account_id'] = CRM_Contribute_PseudoConstant::getRelationalFinancialAccount($this->_id, NULL, 'civicrm_option_value');
142 }
143 if (empty($this->_id) || !CRM_Core_DAO::getFieldValue('CRM_Core_DAO_OptionValue', $this->_id, 'color')) {
144 $defaults['color'] = '#ffffff';
145 }
146 return $defaults;
147 }
148
149 /**
150 * Build the form object.
151 */
152 public function buildQuickForm() {
153 parent::buildQuickForm();
154 $this->setPageTitle(ts('%1 Option', array(1 => $this->_gLabel)));
155
156 if ($this->_action & CRM_Core_Action::DELETE) {
157 return;
158 }
159
160 $this->applyFilter('__ALL__', 'trim');
161
162 $isReserved = FALSE;
163 if ($this->_id) {
164 $isReserved = (bool) CRM_Core_DAO::getFieldValue('CRM_Core_DAO_OptionValue', $this->_id, 'is_reserved');
165 }
166
167 $this->add('text',
168 'label',
169 ts('Label'),
170 CRM_Core_DAO::getAttribute('CRM_Core_DAO_OptionValue', 'label'),
171 TRUE
172 );
173 $domainSpecificOptionGroups = array('from_email_address');
174 $this->_domainSpecific = in_array($this->_gName, $domainSpecificOptionGroups) ? TRUE : FALSE;
175 if ($this->_gName != 'activity_type') {
176 $this->add('text',
177 'value',
178 ts('Value'),
179 CRM_Core_DAO::getAttribute('CRM_Core_DAO_OptionValue', 'value'),
180 TRUE
181 );
182 $this->addRule('value',
183 ts('This Value already exists in the database for this option group. Please select a different Value.'),
184 'optionExists',
185 array('CRM_Core_DAO_OptionValue', $this->_id, $this->_gid, 'value', $this->_domainSpecific)
186 );
187 }
188 else {
189 $this->add('text', 'icon', ts('Icon'), array('class' => 'crm-icon-picker', 'title' => ts('Choose Icon'), 'allowClear' => TRUE));
190 }
191
192 if (in_array($this->_gName, array('activity_status', 'case_status'))) {
193 $this->add('color', 'color', ts('Color'));
194 }
195
196 if (!in_array($this->_gName, array(
197 'email_greeting',
198 'postal_greeting',
199 'addressee',
200 )) && !$isReserved
201 ) {
202 $this->addRule('label',
203 ts('This Label already exists in the database for this option group. Please select a different Label.'),
204 'optionExists',
205 array('CRM_Core_DAO_OptionValue', $this->_id, $this->_gid, 'label', $this->_domainSpecific)
206 );
207 }
208
209 if ($this->_gName == 'case_status') {
210 $classes = array(
211 'Opened' => ts('Opened'),
212 'Closed' => ts('Closed'),
213 );
214
215 $grouping = $this->add('select',
216 'grouping',
217 ts('Status Class'),
218 $classes
219 );
220 if ($isReserved) {
221 $grouping->freeze();
222 }
223 }
224 // CRM-11516
225 if ($this->_gName == 'payment_instrument') {
226 $accountType = CRM_Core_PseudoConstant::accountOptionValues('financial_account_type', NULL, " AND v.name = 'Asset' ");
227 $financialAccount = CRM_Contribute_PseudoConstant::financialAccount(NULL, key($accountType));
228
229 $this->add('select', 'financial_account_id', ts('Financial Account'),
230 array('' => ts('- select -')) + $financialAccount,
231 TRUE
232 );
233 }
234
235 if ($this->_gName == 'activity_status') {
236 $this->add('select',
237 'filter',
238 ts('Status Type'),
239 array(
240 CRM_Activity_BAO_Activity::INCOMPLETE => ts('Incomplete'),
241 CRM_Activity_BAO_Activity::COMPLETED => ts('Completed'),
242 CRM_Activity_BAO_Activity::CANCELLED => ts('Cancelled'),
243 )
244 );
245 }
246 if ($this->_gName == 'redaction_rule') {
247 $this->add('checkbox',
248 'filter',
249 ts('Regular Expression?')
250 );
251 }
252 if ($this->_gName == 'participant_listing') {
253 $this->add('text',
254 'description',
255 ts('Description'),
256 CRM_Core_DAO::getAttribute('CRM_Core_DAO_OptionValue', 'description')
257 );
258 }
259 else {
260 // Hard-coding attributes here since description is still stored as varchar and not text in the schema. dgg
261 $this->add('wysiwyg', 'description',
262 ts('Description'),
263 array('rows' => 4, 'cols' => 80),
264 $this->_gName == 'custom_search'
265 );
266 }
267
268 if ($this->_gName == 'event_badge') {
269 $this->add('text',
270 'name',
271 ts('Class Name'),
272 CRM_Core_DAO::getAttribute('CRM_Core_DAO_OptionValue', 'name')
273 );
274 }
275
276 $this->add('text',
277 'weight',
278 ts('Order'),
279 CRM_Core_DAO::getAttribute('CRM_Core_DAO_OptionValue', 'weight'),
280 TRUE
281 );
282 $this->addRule('weight', ts('is a numeric field'), 'numeric');
283
284 // If CiviCase enabled AND "Add" mode OR "edit" mode for non-reserved activities, only allow user to pick Core or CiviCase component.
285 // FIXME: Each component should define whether adding new activity types is allowed.
286 $config = CRM_Core_Config::singleton();
287 if ($this->_gName == 'activity_type' && in_array("CiviCase", $config->enableComponents) &&
288 (($this->_action & CRM_Core_Action::ADD) || !$isReserved)
289 ) {
290 $caseID = CRM_Core_Component::getComponentID('CiviCase');
291 $components = array('' => ts('Contacts AND Cases'), $caseID => ts('Cases Only'));
292 $this->add('select',
293 'component_id',
294 ts('Component'),
295 $components, FALSE
296 );
297 }
298
299 $enabled = $this->add('checkbox', 'is_active', ts('Enabled?'));
300
301 if ($isReserved) {
302 $enabled->freeze();
303 }
304
305 //fix for CRM-3552, CRM-4575
306 $showIsDefaultGroups = array(
307 'email_greeting',
308 'postal_greeting',
309 'addressee',
310 'from_email_address',
311 'case_status',
312 'encounter_medium',
313 'case_type',
314 'payment_instrument',
315 'communication_style',
316 'soft_credit_type',
317 'website_type',
318 );
319
320 if (in_array($this->_gName, $showIsDefaultGroups)) {
321 $this->assign('showDefault', TRUE);
322 $this->add('checkbox', 'is_default', ts('Default Option?'));
323 }
324
325 //get contact type for which user want to create a new greeting/addressee type, CRM-4575
326 if (in_array($this->_gName, array(
327 'email_greeting',
328 'postal_greeting',
329 'addressee',
330 )) && !$isReserved
331 ) {
332 $values = array(
333 1 => ts('Individual'),
334 2 => ts('Household'),
335 3 => ts('Organization'),
336 4 => ts('Multiple Contact Merge'),
337 );
338 $this->add('select', 'contactOptions', ts('Contact Type'), array('' => '-select-') + $values, TRUE);
339 $this->assign('showContactFilter', TRUE);
340 }
341
342 if ($this->_gName == 'participant_status') {
343 // For Participant Status options, expose the 'filter' field to track which statuses are "Counted", and the Visibility field
344 $element = $this->add('checkbox', 'filter', ts('Counted?'));
345 $this->add('select', 'visibility_id', ts('Visibility'), CRM_Core_PseudoConstant::visibility());
346 }
347 if ($this->_gName == 'participant_role') {
348 // For Participant Role options, expose the 'filter' field to track which statuses are "Counted"
349 $this->add('checkbox', 'filter', ts('Counted?'));
350 }
351
352 $this->addFormRule(array('CRM_Admin_Form_Options', 'formRule'), $this);
353 }
354
355 /**
356 * Global form rule.
357 *
358 * @param array $fields
359 * The input form values.
360 * @param array $files
361 * The uploaded files if any.
362 * @param array $self
363 * Current form object.
364 *
365 * @return array
366 * array of errors / empty array.
367 */
368 public static function formRule($fields, $files, $self) {
369 $errors = array();
370 if ($self->_gName == 'case_status' && empty($fields['grouping'])) {
371 $errors['grouping'] = ts('Status class is a required field');
372 }
373
374 if (in_array($self->_gName, array(
375 'email_greeting',
376 'postal_greeting',
377 'addressee',
378 )) && empty($self->_defaultValues['is_reserved'])
379 ) {
380 $label = $fields['label'];
381 $condition = " AND v.label = '{$label}' ";
382 $values = CRM_Core_OptionGroup::values($self->_gName, FALSE, FALSE, FALSE, $condition, 'filter');
383 $checkContactOptions = TRUE;
384
385 if ($self->_id && ($self->_defaultValues['contactOptions'] == $fields['contactOptions'])) {
386 $checkContactOptions = FALSE;
387 }
388
389 if ($checkContactOptions && in_array($fields['contactOptions'], $values)) {
390 $errors['label'] = ts('This Label already exists in the database for the selected contact type.');
391 }
392 }
393
394 if ($self->_gName == 'from_email_address') {
395 $formEmail = CRM_Utils_Mail::pluckEmailFromHeader($fields['label']);
396 if (!CRM_Utils_Rule::email($formEmail)) {
397 $errors['label'] = ts('Please enter a valid email address.');
398 }
399
400 $formName = explode('"', $fields['label']);
401 if (empty($formName[1]) || count($formName) != 3) {
402 $errors['label'] = ts('Please follow the proper format for From Email Address');
403 }
404 }
405
406 $dataType = self::getOptionGroupDataType($self->_gName);
407 if ($dataType && $self->_gName !== 'activity_type') {
408 $validate = CRM_Utils_Type::validate($fields['value'], $dataType, FALSE);
409 if (!$validate) {
410 CRM_Core_Session::setStatus(
411 ts('Data Type of the value field for this option value does not match ' . $dataType),
412 ts('Value field Data Type mismatch'));
413 }
414 }
415 return $errors;
416 }
417
418 /**
419 * Get the DataType for a specified Option Group.
420 *
421 * @param string $optionGroupName name of the option group
422 *
423 * @return string|null
424 */
425 public static function getOptionGroupDataType($optionGroupName) {
426 $optionGroupId = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_OptionGroup', $optionGroupName, 'id', 'name');
427
428 $dataType = CRM_Core_BAO_OptionGroup::getDataType($optionGroupId);
429 return $dataType;
430 }
431
432 /**
433 * Process the form submission.
434 */
435 public function postProcess() {
436 if ($this->_action & CRM_Core_Action::DELETE) {
437 $fieldValues = array('option_group_id' => $this->_gid);
438 $wt = CRM_Utils_Weight::delWeight('CRM_Core_DAO_OptionValue', $this->_id, $fieldValues);
439
440 if (CRM_Core_BAO_OptionValue::del($this->_id)) {
441 if ($this->_gName == 'phone_type') {
442 CRM_Core_BAO_Phone::setOptionToNull(CRM_Utils_Array::value('value', $this->_defaultValues));
443 }
444
445 CRM_Core_Session::setStatus(ts('Selected %1 type has been deleted.', array(1 => $this->_gLabel)), ts('Record Deleted'), 'success');
446 }
447 else {
448 CRM_Core_Session::setStatus(ts('Selected %1 type has not been deleted.', array(1 => $this->_gLabel)), ts('Sorry'), 'error');
449 CRM_Utils_Weight::correctDuplicateWeights('CRM_Core_DAO_OptionValue', $fieldValues);
450 }
451 }
452 else {
453 $ids = array();
454 $params = $this->exportValues();
455
456 // allow multiple defaults within group.
457 $allowMultiDefaults = array('email_greeting', 'postal_greeting', 'addressee', 'from_email_address');
458 if (in_array($this->_gName, $allowMultiDefaults)) {
459 if ($this->_gName == 'from_email_address') {
460 $params['reset_default_for'] = array('domain_id' => CRM_Core_Config::domainID());
461 }
462 elseif ($filter = CRM_Utils_Array::value('contactOptions', $params)) {
463 $params['filter'] = $filter;
464 $params['reset_default_for'] = array('filter' => "0, " . $params['filter']);
465 }
466
467 //make sure we should has to have space, CRM-6977
468 if ($this->_gName == 'from_email_address') {
469 $params['label'] = str_replace('"<', '" <', $params['label']);
470 }
471 }
472
473 // set value of filter if not present in params
474 if ($this->_id && !array_key_exists('filter', $params)) {
475 if ($this->_gName == 'participant_role') {
476 $params['filter'] = 0;
477 }
478 else {
479 $params['filter'] = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_OptionValue', $this->_id, 'filter', 'id');
480 }
481 }
482
483 if (isset($params['color']) && strtolower($params['color']) == '#ffffff') {
484 $params['color'] = 'null';
485 }
486
487 $groupParams = array('name' => ($this->_gName));
488 $optionValue = CRM_Core_OptionValue::addOptionValue($params, $groupParams, $this->_action, $this->_id);
489
490 // CRM-11516
491 if (!empty($params['financial_account_id'])) {
492 $relationTypeId = key(CRM_Core_PseudoConstant::accountOptionValues('account_relationship', NULL, " AND v.name LIKE 'Asset Account is' "));
493 $params = array(
494 'entity_table' => 'civicrm_option_value',
495 'entity_id' => $optionValue->id,
496 'account_relationship' => $relationTypeId,
497 'financial_account_id' => $params['financial_account_id'],
498 );
499 CRM_Financial_BAO_FinancialTypeAccount::add($params);
500 }
501
502 CRM_Core_Session::setStatus(ts('The %1 \'%2\' has been saved.', array(
503 1 => $this->_gLabel,
504 2 => $optionValue->label,
505 )), ts('Saved'), 'success');
506
507 $this->ajaxResponse['optionValue'] = $optionValue->toArray();
508 }
509 }
510
511 }