ensure that the indexed column is not an FK before deleting the index
[civicrm-core.git] / CRM / Core / Form.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
fee14197 4 | CiviCRM version 5 |
6a488035 5 +--------------------------------------------------------------------+
8c9251b3 6 | Copyright CiviCRM LLC (c) 2004-2018 |
6a488035
TO
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 +--------------------------------------------------------------------+
d25dd0ee 26 */
6a488035
TO
27
28/**
29 * This is our base form. It is part of the Form/Controller/StateMachine
30 * trifecta. Each form is associated with a specific state in the state
31 * machine. Each form can also operate in various modes
32 *
33 * @package CRM
8c9251b3 34 * @copyright CiviCRM LLC (c) 2004-2018
6a488035
TO
35 */
36
37require_once 'HTML/QuickForm/Page.php';
28518c90
EM
38
39/**
40 * Class CRM_Core_Form
41 */
6a488035
TO
42class CRM_Core_Form extends HTML_QuickForm_Page {
43
44 /**
45 * The state object that this form belongs to
46 * @var object
47 */
48 protected $_state;
49
50 /**
51 * The name of this form
52 * @var string
53 */
54 protected $_name;
55
56 /**
57 * The title of this form
58 * @var string
59 */
60 protected $_title = NULL;
61
73afe1e6 62 /**
63 * The default values for the form.
64 *
65 * @var array
66 */
67 public $_defaults = array();
68
6a488035 69 /**
2eee184e
TO
70 * (QUASI-PROTECTED) The options passed into this form
71 *
72 * This field should marked `protected` and is not generally
73 * intended for external callers, but some edge-cases do use it.
74 *
6a488035
TO
75 * @var mixed
76 */
2eee184e 77 public $_options = NULL;
6a488035
TO
78
79 /**
2eee184e
TO
80 * (QUASI-PROTECTED) The mode of operation for this form
81 *
82 * This field should marked `protected` and is not generally
83 * intended for external callers, but some edge-cases do use it.
84 *
6a488035
TO
85 * @var int
86 */
2eee184e 87 public $_action;
6a488035 88
1b9f9ca3
EM
89 /**
90 * Available payment processors.
91 *
92 * As part of trying to consolidate various payment pages we store processors here & have functions
93 * at this level to manage them.
94 *
95 * @var array
96 * An array of payment processor details with objects loaded in the 'object' field.
97 */
42e3a033 98 protected $_paymentProcessors;
1b9f9ca3
EM
99
100 /**
101 * Available payment processors (IDS).
102 *
103 * As part of trying to consolidate various payment pages we store processors here & have functions
cbcb5b49 104 * at this level to manage them. An alternative would be to have a separate Form that is inherited
105 * by all forms that allow payment processing.
1b9f9ca3
EM
106 *
107 * @var array
108 * An array of the IDS available on this form.
109 */
110 public $_paymentProcessorIDs;
111
cbcb5b49 112 /**
113 * Default or selected processor id.
114 *
115 * As part of trying to consolidate various payment pages we store processors here & have functions
116 * at this level to manage them. An alternative would be to have a separate Form that is inherited
117 * by all forms that allow payment processing.
118 *
119 * @var int
120 */
121 protected $_paymentProcessorID;
122
123 /**
124 * Is pay later enabled for the form.
125 *
126 * As part of trying to consolidate various payment pages we store processors here & have functions
127 * at this level to manage them. An alternative would be to have a separate Form that is inherited
128 * by all forms that allow payment processing.
129 *
130 * @var int
131 */
132 protected $_is_pay_later_enabled;
133
6a488035 134 /**
100fef9d 135 * The renderer used for this form
6a488035
TO
136 *
137 * @var object
138 */
139 protected $_renderer;
140
5d86176b 141 /**
142 * An array to hold a list of datefields on the form
143 * so that they can be converted to ISO in a consistent manner
144 *
145 * @var array
146 *
147 * e.g on a form declare $_dateFields = array(
148 * 'receive_date' => array('default' => 'now'),
149 * );
150 * then in postProcess call $this->convertDateFieldsToMySQL($formValues)
151 * to have the time field re-incorporated into the field & 'now' set if
152 * no value has been passed in
153 */
154 protected $_dateFields = array();
155
6a488035 156 /**
100fef9d 157 * Cache the smarty template for efficiency reasons
6a488035
TO
158 *
159 * @var CRM_Core_Smarty
160 */
161 static protected $_template;
162
461fa5fb 163 /**
164 * Indicate if this form should warn users of unsaved changes
165 */
166 protected $unsavedChangesWarn;
167
03a7ec8f 168 /**
fc05b8da 169 * What to return to the client if in ajax mode (snippet=json)
03a7ec8f
CW
170 *
171 * @var array
172 */
173 public $ajaxResponse = array();
174
118e964e
CW
175 /**
176 * Url path used to reach this page
177 *
178 * @var array
179 */
180 public $urlPath = array();
181
d77a0a58
EM
182 /**
183 * @var CRM_Core_Controller
184 */
185 public $controller;
4a44fd8a 186
6a488035 187 /**
100fef9d 188 * Constants for attributes for various form elements
6a488035
TO
189 * attempt to standardize on the number of variations that we
190 * use of the below form elements
191 *
192 * @var const string
193 */
7da04cde 194 const ATTR_SPACING = '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;';
6a488035
TO
195
196 /**
197 * All checkboxes are defined with a common prefix. This allows us to
198 * have the same javascript to check / clear all the checkboxes etc
199 * If u have multiple groups of checkboxes, you will need to give them different
200 * ids to avoid potential name collision
201 *
fffe9ee1 202 * @var string|int
6a488035 203 */
7da04cde 204 const CB_PREFIX = 'mark_x_', CB_PREFIY = 'mark_y_', CB_PREFIZ = 'mark_z_', CB_PREFIX_LEN = 7;
6a488035 205
1d07e7ab
CW
206 /**
207 * @internal to keep track of chain-select fields
208 * @var array
209 */
210 private $_chainSelectFields = array();
211
d8f1758d
CW
212 /**
213 * Extra input types we support via the "add" method
214 * @var array
215 */
216 public static $html5Types = array(
217 'number',
218 'url',
219 'email',
1192bd09 220 'color',
d8f1758d
CW
221 );
222
6a488035 223 /**
4b62bc4f 224 * Constructor for the basic form page.
6a488035
TO
225 *
226 * We should not use QuickForm directly. This class provides a lot
227 * of default convenient functions, rules and buttons
228 *
6a0b768e
TO
229 * @param object $state
230 * State associated with this form.
fffe9ee1 231 * @param \const|\enum|int $action The mode the form is operating in (None/Create/View/Update/Delete)
6a0b768e
TO
232 * @param string $method
233 * The type of http method used (GET/POST).
234 * @param string $name
235 * The name of the form if different from class name.
6a488035 236 *
dd244018 237 * @return \CRM_Core_Form
6a488035 238 */
2da40d21 239 public function __construct(
6a488035
TO
240 $state = NULL,
241 $action = CRM_Core_Action::NONE,
242 $method = 'post',
f9f40af3 243 $name = NULL
6a488035
TO
244 ) {
245
246 if ($name) {
247 $this->_name = $name;
248 }
249 else {
b50fdacc 250 // CRM-15153 - FIXME this name translates to a DOM id and is not always unique!
6a488035
TO
251 $this->_name = CRM_Utils_String::getClassName(CRM_Utils_System::getClassName($this));
252 }
253
6ef04c72 254 parent::__construct($this->_name, $method);
6a488035
TO
255
256 $this->_state =& $state;
257 if ($this->_state) {
258 $this->_state->setName($this->_name);
259 }
260 $this->_action = (int) $action;
261
262 $this->registerRules();
263
264 // let the constructor initialize this, should happen only once
265 if (!isset(self::$_template)) {
266 self::$_template = CRM_Core_Smarty::singleton();
267 }
b50fdacc
CW
268 // Workaround for CRM-15153 - give each form a reasonably unique css class
269 $this->addClass(CRM_Utils_System::getClassName($this));
03a7ec8f 270
819d4cbb 271 $this->assign('snippet', CRM_Utils_Array::value('snippet', $_GET));
d84ae5f6 272 $this->setTranslatedFields();
6a488035
TO
273 }
274
d84ae5f6 275 /**
276 * Set translated fields.
277 *
278 * This function is called from the class constructor, allowing us to set
279 * fields on the class that can't be set as properties due to need for
280 * translation or other non-input specific handling.
281 */
282 protected function setTranslatedFields() {}
283
023e90c3 284 /**
e51d62d3 285 * Add one or more css classes to the form.
286 *
100fef9d 287 * @param string $className
023e90c3
CW
288 */
289 public function addClass($className) {
290 $classes = $this->getAttribute('class');
291 $this->setAttribute('class', ($classes ? "$classes " : '') . $className);
292 }
293
6a488035 294 /**
fe482240 295 * Register all the standard rules that most forms potentially use.
6a488035 296 */
00be9182 297 public function registerRules() {
6a488035 298 static $rules = array(
353ffa53
TO
299 'title',
300 'longTitle',
301 'variable',
302 'qfVariable',
303 'phone',
304 'integer',
305 'query',
306 'url',
307 'wikiURL',
308 'domain',
309 'numberOfDigit',
310 'date',
311 'currentDate',
312 'asciiFile',
313 'htmlFile',
314 'utf8File',
315 'objectExists',
316 'optionExists',
317 'postalCode',
318 'money',
319 'positiveInteger',
320 'xssString',
321 'fileExists',
d9d7e7dd 322 'settingPath',
353ffa53
TO
323 'autocomplete',
324 'validContact',
6a488035
TO
325 );
326
327 foreach ($rules as $rule) {
328 $this->registerRule($rule, 'callback', $rule, 'CRM_Utils_Rule');
329 }
330 }
331
332 /**
e51d62d3 333 * Simple easy to use wrapper around addElement.
334 *
335 * Deal with simple validation rules.
6a488035 336 *
bae056c6
CW
337 * @param string $type
338 * @param string $name
77b97be7 339 * @param string $label
bae056c6 340 * @param string|array $attributes (options for select elements)
77b97be7 341 * @param bool $required
6a0b768e
TO
342 * @param array $extra
343 * (attributes for select elements).
d86e674f 344 * For datepicker elements this is consistent with the data
345 * from CRM_Utils_Date::getDatePickerExtra
6a488035 346 *
e51d62d3 347 * @return HTML_QuickForm_Element
348 * Could be an error object
6a488035 349 */
2da40d21 350 public function &add(
f9f40af3 351 $type, $name, $label = '',
908fe4e6 352 $attributes = '', $required = FALSE, $extra = NULL
6a488035 353 ) {
092cb9c5 354 // Fudge some extra types that quickform doesn't support
1192bd09 355 $inputType = $type;
d8f1758d 356 if ($type == 'wysiwyg' || in_array($type, self::$html5Types)) {
66659cd7 357 $attributes = ($attributes ? $attributes : array()) + array('class' => '');
092cb9c5 358 $attributes['class'] = ltrim($attributes['class'] . " crm-form-$type");
7ad5ae6a
CW
359 if ($type == 'wysiwyg' && isset($attributes['preset'])) {
360 $attributes['data-preset'] = $attributes['preset'];
361 unset($attributes['preset']);
362 }
092cb9c5 363 $type = $type == 'wysiwyg' ? 'textarea' : 'text';
b608cfb1 364 }
b733747a
CW
365 // Like select but accepts rich array data (with nesting, colors, icons, etc) as option list.
366 if ($inputType == 'select2') {
367 $type = 'text';
368 $options = $attributes;
369 $attributes = $attributes = ($extra ? $extra : array()) + array('class' => '');
370 $attributes['class'] = ltrim($attributes['class'] . " crm-select2 crm-form-select2");
371 $attributes['data-select-params'] = json_encode(array('data' => $options, 'multiple' => !empty($attributes['multiple'])));
372 unset($attributes['multiple']);
373 $extra = NULL;
374 }
a2973721 375 // @see http://wiki.civicrm.org/confluence/display/CRMDOC/crmDatepicker
238fee7f 376 if ($type == 'datepicker') {
a2973721 377 $attributes = ($attributes ? $attributes : array());
238fee7f 378 $attributes['data-crm-datepicker'] = json_encode((array) $extra);
1f1410dc 379 if (!empty($attributes['aria-label']) || $label) {
380 $attributes['aria-label'] = CRM_Utils_Array::value('aria-label', $attributes, $label);
381 }
238fee7f
CW
382 $type = "text";
383 }
1d07e7ab 384 if ($type == 'select' && is_array($extra)) {
bae056c6 385 // Normalize this property
1d07e7ab
CW
386 if (!empty($extra['multiple'])) {
387 $extra['multiple'] = 'multiple';
388 }
389 else {
390 unset($extra['multiple']);
391 }
65e8615b 392 unset($extra['size'], $extra['maxlength']);
bae056c6
CW
393 // Add placeholder option for select
394 if (isset($extra['placeholder'])) {
395 if ($extra['placeholder'] === TRUE) {
396 $extra['placeholder'] = $required ? ts('- select -') : ts('- none -');
397 }
398 if (($extra['placeholder'] || $extra['placeholder'] === '') && empty($extra['multiple']) && is_array($attributes) && !isset($attributes[''])) {
399 $attributes = array('' => $extra['placeholder']) + $attributes;
400 }
401 }
908fe4e6
CW
402 }
403 $element = $this->addElement($type, $name, $label, $attributes, $extra);
6a488035
TO
404 if (HTML_QuickForm::isError($element)) {
405 CRM_Core_Error::fatal(HTML_QuickForm::errorMessage($element));
406 }
407
1192bd09
CW
408 if ($inputType == 'color') {
409 $this->addRule($name, ts('%1 must contain a color value e.g. #ffffff.', array(1 => $label)), 'regex', '/#[0-9a-fA-F]{6}/');
410 }
411
6a488035
TO
412 if ($required) {
413 if ($type == 'file') {
414 $error = $this->addRule($name, ts('%1 is a required field.', array(1 => $label)), 'uploadedfile');
415 }
416 else {
417 $error = $this->addRule($name, ts('%1 is a required field.', array(1 => $label)), 'required');
418 }
419 if (HTML_QuickForm::isError($error)) {
420 CRM_Core_Error::fatal(HTML_QuickForm::errorMessage($element));
421 }
422 }
423
b3ee84c9
MD
424 // Add context for the editing of option groups
425 if (isset($extra['option_context'])) {
426 $context = json_encode($extra['option_context']);
427 $element->setAttribute('data-option-edit-context', $context);
428 }
429
6a488035
TO
430 return $element;
431 }
432
433 /**
e51d62d3 434 * Preprocess form.
435 *
436 * This is called before buildForm. Any pre-processing that
437 * needs to be done for buildForm should be done here.
6a488035 438 *
8eedd10a 439 * This is a virtual function and should be redefined if needed.
6a488035 440 */
f9f40af3
TO
441 public function preProcess() {
442 }
6a488035
TO
443
444 /**
8eedd10a 445 * Called after the form is validated.
446 *
447 * Any processing of form state etc should be done in this function.
6a488035
TO
448 * Typically all processing associated with a form should be done
449 * here and relevant state should be stored in the session
450 *
451 * This is a virtual function and should be redefined if needed
6a488035 452 */
f9f40af3
TO
453 public function postProcess() {
454 }
6a488035
TO
455
456 /**
e51d62d3 457 * Main process wrapper.
458 *
459 * Implemented so that we can call all the hook functions.
460 *
6a0b768e
TO
461 * @param bool $allowAjax
462 * FIXME: This feels kind of hackish, ideally we would take the json-related code from this function.
7e9fdecf 463 * and bury it deeper down in the controller
6a488035 464 */
00be9182 465 public function mainProcess($allowAjax = TRUE) {
6a488035 466 $this->postProcess();
6a488035 467 $this->postProcessHook();
03a7ec8f 468
fc05b8da 469 // Respond with JSON if in AJAX context (also support legacy value '6')
353ffa53
TO
470 if ($allowAjax && !empty($_REQUEST['snippet']) && in_array($_REQUEST['snippet'], array(
471 CRM_Core_Smarty::PRINT_JSON,
af9b09df 472 6,
353ffa53
TO
473 ))
474 ) {
03a7ec8f
CW
475 $this->ajaxResponse['buttonName'] = str_replace('_qf_' . $this->getAttribute('id') . '_', '', $this->controller->getButtonName());
476 $this->ajaxResponse['action'] = $this->_action;
18ddc127
CW
477 if (isset($this->_id) || isset($this->id)) {
478 $this->ajaxResponse['id'] = isset($this->id) ? $this->id : $this->_id;
479 }
03a7ec8f
CW
480 CRM_Core_Page_AJAX::returnJsonResponse($this->ajaxResponse);
481 }
6a488035
TO
482 }
483
484 /**
4b62bc4f 485 * The postProcess hook is typically called by the framework.
e51d62d3 486 *
6a488035
TO
487 * However in a few cases, the form exits or redirects early in which
488 * case it needs to call this function so other modules can do the needful
489 * Calling this function directly should be avoided if possible. In general a
490 * better way is to do setUserContext so the framework does the redirect
6a488035 491 */
00be9182 492 public function postProcessHook() {
6a488035
TO
493 CRM_Utils_Hook::postProcess(get_class($this), $this);
494 }
495
496 /**
8eedd10a 497 * This virtual function is used to build the form.
6a488035 498 *
8eedd10a 499 * It replaces the buildForm associated with QuickForm_Page. This allows us to put
500 * preProcess in front of the actual form building routine
6a488035 501 */
f9f40af3
TO
502 public function buildQuickForm() {
503 }
6a488035
TO
504
505 /**
8eedd10a 506 * This virtual function is used to set the default values of various form elements.
6a488035 507 *
a1a2a83d 508 * @return array|NULL
a6c01b45 509 * reference to the array of default values
6a488035 510 */
f9f40af3 511 public function setDefaultValues() {
a1a2a83d 512 return NULL;
f9f40af3 513 }
6a488035
TO
514
515 /**
8eedd10a 516 * This is a virtual function that adds group and global rules to the form.
6a488035 517 *
8eedd10a 518 * Keeping it distinct from the form to keep code small
519 * and localized in the form building code
6a488035 520 */
f9f40af3
TO
521 public function addRules() {
522 }
6a488035 523
b5c2afd0 524 /**
fe482240 525 * Performs the server side validation.
b5c2afd0 526 * @since 1.0
5c766a0b 527 * @return bool
a6c01b45 528 * true if no error found
b5c2afd0
EM
529 * @throws HTML_QuickForm_Error
530 */
00be9182 531 public function validate() {
6a488035
TO
532 $error = parent::validate();
533
bc999cd1
CW
534 $this->validateChainSelectFields();
535
6a9e89b4 536 $hookErrors = array();
6a488035
TO
537
538 CRM_Utils_Hook::validateForm(
539 get_class($this),
540 $this->_submitValues,
541 $this->_submitFiles,
542 $this,
543 $hookErrors
544 );
545
546 if (!empty($hookErrors)) {
547 $this->_errors += $hookErrors;
548 }
549
550 return (0 == count($this->_errors));
551 }
552
553 /**
3bdf1f3a 554 * Core function that builds the form.
555 *
556 * We redefine this function here and expect all CRM forms to build their form in the function
6a488035 557 * buildQuickForm.
6a488035 558 */
00be9182 559 public function buildForm() {
6a488035
TO
560 $this->_formBuilt = TRUE;
561
562 $this->preProcess();
563
21d2903d
AN
564 CRM_Utils_Hook::preProcess(get_class($this), $this);
565
6a488035
TO
566 $this->assign('translatePermission', CRM_Core_Permission::check('translate CiviCRM'));
567
568 if (
569 $this->controller->_key &&
570 $this->controller->_generateQFKey
571 ) {
572 $this->addElement('hidden', 'qfKey', $this->controller->_key);
573 $this->assign('qfKey', $this->controller->_key);
ab435bd4 574
6a488035
TO
575 }
576
ab435bd4
DL
577 // _generateQFKey suppresses the qfKey generation on form snippets that
578 // are part of other forms, hence we use that to avoid adding entryURL
579 if ($this->controller->_generateQFKey && $this->controller->_entryURL) {
3ab88a8c
DL
580 $this->addElement('hidden', 'entryURL', $this->controller->_entryURL);
581 }
6a488035
TO
582
583 $this->buildQuickForm();
584
585 $defaults = $this->setDefaultValues();
586 unset($defaults['qfKey']);
587
588 if (!empty($defaults)) {
589 $this->setDefaults($defaults);
590 }
591
592 // call the form hook
b44e3f84 593 // also call the hook function so any modules can set their own custom defaults
6a488035
TO
594 // the user can do both the form and set default values with this hook
595 CRM_Utils_Hook::buildForm(get_class($this), $this);
596
597 $this->addRules();
3e201321 598
599 //Set html data-attribute to enable warning user of unsaved changes
f9f40af3 600 if ($this->unsavedChangesWarn === TRUE
353ffa53
TO
601 || (!isset($this->unsavedChangesWarn)
602 && ($this->_action & CRM_Core_Action::ADD || $this->_action & CRM_Core_Action::UPDATE)
603 )
604 ) {
f9f40af3 605 $this->setAttribute('data-warn-changes', 'true');
3e201321 606 }
6a488035
TO
607 }
608
609 /**
3bdf1f3a 610 * Add default Next / Back buttons.
6a488035 611 *
6c552737
TO
612 * @param array $params
613 * Array of associative arrays in the order in which the buttons should be
614 * displayed. The associate array has 3 fields: 'type', 'name' and 'isDefault'
615 * The base form class will define a bunch of static arrays for commonly used
616 * formats.
6a488035 617 */
00be9182 618 public function addButtons($params) {
deae896d 619 $prevnext = $spacing = array();
6a488035 620 foreach ($params as $button) {
fdb0ca2c 621 $attrs = array('class' => 'crm-form-submit') + (array) CRM_Utils_Array::value('js', $button);
6a488035 622
b01812e5
CW
623 if (!empty($button['class'])) {
624 $attrs['class'] .= ' ' . $button['class'];
625 }
626
fdb0ca2c
CW
627 if (!empty($button['isDefault'])) {
628 $attrs['class'] .= ' default';
6a488035
TO
629 }
630
fdb0ca2c
CW
631 if (in_array($button['type'], array('upload', 'next', 'submit', 'done', 'process', 'refresh'))) {
632 $attrs['class'] .= ' validate';
f62db3ac 633 $defaultIcon = 'fa-check';
fdb0ca2c
CW
634 }
635 else {
b61fd8cf 636 $attrs['class'] .= ' cancel';
f62db3ac 637 $defaultIcon = $button['type'] == 'back' ? 'fa-chevron-left' : 'fa-times';
b61fd8cf
CW
638 }
639
6a488035
TO
640 if ($button['type'] === 'reset') {
641 $prevnext[] = $this->createElement($button['type'], 'reset', $button['name'], $attrs);
642 }
643 else {
a7488080 644 if (!empty($button['subName'])) {
deae896d 645 if ($button['subName'] == 'new') {
f62db3ac 646 $defaultIcon = 'fa-plus-circle';
deae896d 647 }
fdb0ca2c 648 if ($button['subName'] == 'done') {
f62db3ac 649 $defaultIcon = 'fa-check-circle';
fdb0ca2c
CW
650 }
651 if ($button['subName'] == 'next') {
f62db3ac 652 $defaultIcon = 'fa-chevron-right';
fdb0ca2c 653 }
6a488035
TO
654 }
655
b61fd8cf 656 if (in_array($button['type'], array('next', 'upload', 'done')) && $button['name'] === ts('Save')) {
fdb0ca2c 657 $attrs['accesskey'] = 'S';
6a488035 658 }
deae896d
CW
659 $icon = CRM_Utils_Array::value('icon', $button, $defaultIcon);
660 if ($icon) {
661 $attrs['crm-icon'] = $icon;
662 }
fdb0ca2c 663 $buttonName = $this->getButtonName($button['type'], CRM_Utils_Array::value('subName', $button));
6a488035
TO
664 $prevnext[] = $this->createElement('submit', $buttonName, $button['name'], $attrs);
665 }
a7488080 666 if (!empty($button['isDefault'])) {
6a488035
TO
667 $this->setDefaultAction($button['type']);
668 }
669
670 // if button type is upload, set the enctype
671 if ($button['type'] == 'upload') {
672 $this->updateAttributes(array('enctype' => 'multipart/form-data'));
673 $this->setMaxFileSize();
674 }
675
676 // hack - addGroup uses an array to express variable spacing, read from the last element
677 $spacing[] = CRM_Utils_Array::value('spacing', $button, self::ATTR_SPACING);
678 }
679 $this->addGroup($prevnext, 'buttons', '', $spacing, FALSE);
680 }
681
682 /**
fe482240 683 * Getter function for Name.
6a488035
TO
684 *
685 * @return string
6a488035 686 */
00be9182 687 public function getName() {
6a488035
TO
688 return $this->_name;
689 }
690
691 /**
fe482240 692 * Getter function for State.
6a488035
TO
693 *
694 * @return object
6a488035 695 */
00be9182 696 public function &getState() {
6a488035
TO
697 return $this->_state;
698 }
699
700 /**
fe482240 701 * Getter function for StateType.
6a488035
TO
702 *
703 * @return int
6a488035 704 */
00be9182 705 public function getStateType() {
6a488035
TO
706 return $this->_state->getType();
707 }
708
709 /**
3bdf1f3a 710 * Getter function for title.
711 *
712 * Should be over-ridden by derived class.
6a488035
TO
713 *
714 * @return string
6a488035 715 */
00be9182 716 public function getTitle() {
6a488035
TO
717 return $this->_title ? $this->_title : ts('ERROR: Title is not Set');
718 }
719
720 /**
100fef9d 721 * Setter function for title.
6a488035 722 *
6a0b768e
TO
723 * @param string $title
724 * The title of the form.
6a488035 725 */
00be9182 726 public function setTitle($title) {
6a488035
TO
727 $this->_title = $title;
728 }
729
8345c9d3
EM
730 /**
731 * Assign billing type id to bltID.
732 *
733 * @throws CRM_Core_Exception
734 */
735 public function assignBillingType() {
b576d770 736 $this->_bltID = CRM_Core_BAO_LocationType::getBilling();
8345c9d3
EM
737 $this->set('bltID', $this->_bltID);
738 $this->assign('bltID', $this->_bltID);
739 }
740
1b9f9ca3
EM
741 /**
742 * This if a front end form function for setting the payment processor.
743 *
744 * It would be good to sync it with the back-end function on abstractEditPayment & use one everywhere.
745 *
682c12c0 746 * @param bool $isPayLaterEnabled
cbcb5b49 747 *
1b9f9ca3
EM
748 * @throws \CRM_Core_Exception
749 */
682c12c0 750 protected function assignPaymentProcessor($isPayLaterEnabled) {
1b9f9ca3
EM
751 $this->_paymentProcessors = CRM_Financial_BAO_PaymentProcessor::getPaymentProcessors(
752 array(ucfirst($this->_mode) . 'Mode'),
753 $this->_paymentProcessorIDs
754 );
682c12c0
JP
755 if ($isPayLaterEnabled) {
756 $this->_paymentProcessors[0] = CRM_Financial_BAO_PaymentProcessor::getPayment(0);
757 }
1b9f9ca3
EM
758
759 if (!empty($this->_paymentProcessors)) {
760 foreach ($this->_paymentProcessors as $paymentProcessorID => $paymentProcessorDetail) {
761 if (empty($this->_paymentProcessor) && $paymentProcessorDetail['is_default'] == 1 || (count($this->_paymentProcessors) == 1)
762 ) {
763 $this->_paymentProcessor = $paymentProcessorDetail;
764 $this->assign('paymentProcessor', $this->_paymentProcessor);
765 // Setting this is a bit of a legacy overhang.
766 $this->_paymentObject = $paymentProcessorDetail['object'];
767 }
768 }
769 // It's not clear why we set this on the form.
770 $this->set('paymentProcessors', $this->_paymentProcessors);
771 }
772 else {
773 throw new CRM_Core_Exception(ts('A payment processor configured for this page might be disabled (contact the site administrator for assistance).'));
774 }
f48e6cf7 775
1b9f9ca3
EM
776 }
777
bddc8a28 778 /**
779 * Format the fields for the payment processor.
780 *
781 * In order to pass fields to the payment processor in a consistent way we add some renamed
782 * parameters.
783 *
784 * @param array $fields
785 *
786 * @return array
787 */
788 protected function formatParamsForPaymentProcessor($fields) {
789 // also add location name to the array
790 $this->_params["address_name-{$this->_bltID}"] = CRM_Utils_Array::value('billing_first_name', $this->_params) . ' ' . CRM_Utils_Array::value('billing_middle_name', $this->_params) . ' ' . CRM_Utils_Array::value('billing_last_name', $this->_params);
791 $this->_params["address_name-{$this->_bltID}"] = trim($this->_params["address_name-{$this->_bltID}"]);
792 // Add additional parameters that the payment processors are used to receiving.
793 if (!empty($this->_params["billing_state_province_id-{$this->_bltID}"])) {
794 $this->_params['state_province'] = $this->_params["state_province-{$this->_bltID}"] = $this->_params["billing_state_province-{$this->_bltID}"] = CRM_Core_PseudoConstant::stateProvinceAbbreviation($this->_params["billing_state_province_id-{$this->_bltID}"]);
795 }
796 if (!empty($this->_params["billing_country_id-{$this->_bltID}"])) {
797 $this->_params['country'] = $this->_params["country-{$this->_bltID}"] = $this->_params["billing_country-{$this->_bltID}"] = CRM_Core_PseudoConstant::countryIsoCode($this->_params["billing_country_id-{$this->_bltID}"]);
798 }
799
800 list($hasAddressField, $addressParams) = CRM_Contribute_BAO_Contribution::getPaymentProcessorReadyAddressParams($this->_params, $this->_bltID);
801 if ($hasAddressField) {
802 $this->_params = array_merge($this->_params, $addressParams);
803 }
804
805 $nameFields = array('first_name', 'middle_name', 'last_name');
806 foreach ($nameFields as $name) {
807 $fields[$name] = 1;
808 if (array_key_exists("billing_$name", $this->_params)) {
809 $this->_params[$name] = $this->_params["billing_{$name}"];
810 $this->_params['preserveDBName'] = TRUE;
811 }
812 }
813 return $fields;
814 }
815
42e3a033
EM
816 /**
817 * Handle Payment Processor switching for contribution and event registration forms.
818 *
819 * This function is shared between contribution & event forms & this is their common class.
820 *
821 * However, this should be seen as an in-progress refactor, the end goal being to also align the
822 * backoffice forms that action payments.
823 *
824 * This function overlaps assignPaymentProcessor, in a bad way.
825 */
826 protected function preProcessPaymentOptions() {
827 $this->_paymentProcessorID = NULL;
828 if ($this->_paymentProcessors) {
829 if (!empty($this->_submitValues)) {
830 $this->_paymentProcessorID = CRM_Utils_Array::value('payment_processor_id', $this->_submitValues);
831 $this->_paymentProcessor = CRM_Utils_Array::value($this->_paymentProcessorID, $this->_paymentProcessors);
832 $this->set('type', $this->_paymentProcessorID);
833 $this->set('mode', $this->_mode);
834 $this->set('paymentProcessor', $this->_paymentProcessor);
835 }
836 // Set default payment processor
837 else {
838 foreach ($this->_paymentProcessors as $values) {
839 if (!empty($values['is_default']) || count($this->_paymentProcessors) == 1) {
840 $this->_paymentProcessorID = $values['id'];
841 break;
842 }
843 }
844 }
1d1fee72 845 if ($this->_paymentProcessorID
846 || (isset($this->_submitValues['payment_processor_id']) && $this->_submitValues['payment_processor_id'] == 0)
847 ) {
42e3a033
EM
848 CRM_Core_Payment_ProcessorForm::preProcess($this);
849 }
850 else {
851 $this->_paymentProcessor = array();
852 }
cb5962bd 853 CRM_Financial_Form_Payment::addCreditCardJs($this->_paymentProcessorID);
42e3a033
EM
854 }
855 $this->assign('paymentProcessorID', $this->_paymentProcessorID);
f48e6cf7 856 // We save the fact that the profile 'billing' is required on the payment form.
857 // Currently pay-later is the only 'processor' that takes notice of this - but ideally
858 // 1) it would be possible to select the minimum_billing_profile_id for the contribution form
859 // 2) that profile_id would be set on the payment processor
860 // 3) the payment processor would return a billing form that combines these user-configured
861 // minimums with the payment processor minimums. This would lead to fields like 'postal_code'
862 // only being on the form if either the admin has configured it as wanted or the processor
863 // requires it.
864 $this->assign('billing_profile_id', (CRM_Utils_Array::value('is_billing_required', $this->_values) ? 'billing' : ''));
42e3a033 865 }
1b9f9ca3 866
ec022878 867 /**
868 * Handle pre approval for processors.
869 *
870 * This fits with the flow where a pre-approval is done and then confirmed in the next stage when confirm is hit.
871 *
872 * This function is shared between contribution & event forms & this is their common class.
873 *
874 * However, this should be seen as an in-progress refactor, the end goal being to also align the
875 * backoffice forms that action payments.
876 *
877 * @param array $params
878 */
879 protected function handlePreApproval(&$params) {
880 try {
881 $payment = Civi\Payment\System::singleton()->getByProcessor($this->_paymentProcessor);
882 $params['component'] = 'contribute';
883 $result = $payment->doPreApproval($params);
884 if (empty($result)) {
885 // This could happen, for example, when paypal looks at the button value & decides it is not paypal express.
886 return;
887 }
888 }
889 catch (\Civi\Payment\Exception\PaymentProcessorException $e) {
abfb35ee 890 CRM_Core_Error::statusBounce(ts('Payment approval failed with message :') . $e->getMessage(), $payment->getCancelUrl($params['qfKey'], CRM_Utils_Array::value('participant_id', $params)));
ec022878 891 }
892
893 $this->set('pre_approval_parameters', $result['pre_approval_parameters']);
894 if (!empty($result['redirect_url'])) {
895 CRM_Utils_System::redirect($result['redirect_url']);
896 }
897 }
898
6a488035 899 /**
fe482240 900 * Setter function for options.
6a488035 901 *
6c552737 902 * @param mixed $options
6a488035 903 */
00be9182 904 public function setOptions($options) {
6a488035
TO
905 $this->_options = $options;
906 }
907
6a488035 908 /**
fe482240 909 * Render form and return contents.
6a488035
TO
910 *
911 * @return string
6a488035 912 */
00be9182 913 public function toSmarty() {
1d07e7ab 914 $this->preProcessChainSelectFields();
6a488035
TO
915 $renderer = $this->getRenderer();
916 $this->accept($renderer);
917 $content = $renderer->toArray();
918 $content['formName'] = $this->getName();
b50fdacc
CW
919 // CRM-15153
920 $content['formClass'] = CRM_Utils_System::getClassName($this);
6a488035
TO
921 return $content;
922 }
923
924 /**
3bdf1f3a 925 * Getter function for renderer.
926 *
927 * If renderer is not set create one and initialize it.
6a488035
TO
928 *
929 * @return object
6a488035 930 */
00be9182 931 public function &getRenderer() {
6a488035
TO
932 if (!isset($this->_renderer)) {
933 $this->_renderer = CRM_Core_Form_Renderer::singleton();
934 }
935 return $this->_renderer;
936 }
937
938 /**
fe482240 939 * Use the form name to create the tpl file name.
6a488035
TO
940 *
941 * @return string
6a488035 942 */
00be9182 943 public function getTemplateFileName() {
6a488035
TO
944 $ext = CRM_Extension_System::singleton()->getMapper();
945 if ($ext->isExtensionClass(CRM_Utils_System::getClassName($this))) {
946 $filename = $ext->getTemplateName(CRM_Utils_System::getClassName($this));
947 $tplname = $ext->getTemplatePath(CRM_Utils_System::getClassName($this)) . DIRECTORY_SEPARATOR . $filename;
948 }
949 else {
9b591d79
TO
950 $tplname = strtr(
951 CRM_Utils_System::getClassName($this),
952 array(
953 '_' => DIRECTORY_SEPARATOR,
954 '\\' => DIRECTORY_SEPARATOR,
955 )
956 ) . '.tpl';
6a488035
TO
957 }
958 return $tplname;
959 }
960
8aac22c8 961 /**
3bdf1f3a 962 * A wrapper for getTemplateFileName.
963 *
964 * This includes calling the hook to prevent us from having to copy & paste the logic of calling the hook.
8aac22c8 965 */
00be9182 966 public function getHookedTemplateFileName() {
8aac22c8 967 $pageTemplateFile = $this->getTemplateFileName();
968 CRM_Utils_Hook::alterTemplateFile(get_class($this), $this, 'page', $pageTemplateFile);
969 return $pageTemplateFile;
970 }
971
6a488035 972 /**
3bdf1f3a 973 * Default extra tpl file basically just replaces .tpl with .extra.tpl.
974 *
975 * i.e. we do not override.
6a488035
TO
976 *
977 * @return string
6a488035 978 */
00be9182 979 public function overrideExtraTemplateFileName() {
6a488035
TO
980 return NULL;
981 }
982
983 /**
fe482240 984 * Error reporting mechanism.
6a488035 985 *
6a0b768e
TO
986 * @param string $message
987 * Error Message.
988 * @param int $code
989 * Error Code.
990 * @param CRM_Core_DAO $dao
991 * A data access object on which we perform a rollback if non - empty.
6a488035 992 */
00be9182 993 public function error($message, $code = NULL, $dao = NULL) {
6a488035
TO
994 if ($dao) {
995 $dao->query('ROLLBACK');
996 }
997
998 $error = CRM_Core_Error::singleton();
999
1000 $error->push($code, $message);
1001 }
1002
1003 /**
fe482240 1004 * Store the variable with the value in the form scope.
6a488035 1005 *
6c552737
TO
1006 * @param string $name
1007 * Name of the variable.
1008 * @param mixed $value
1009 * Value of the variable.
6a488035 1010 */
00be9182 1011 public function set($name, $value) {
6a488035
TO
1012 $this->controller->set($name, $value);
1013 }
1014
1015 /**
fe482240 1016 * Get the variable from the form scope.
6a488035 1017 *
6c552737
TO
1018 * @param string $name
1019 * Name of the variable
6a488035
TO
1020 *
1021 * @return mixed
6a488035 1022 */
00be9182 1023 public function get($name) {
6a488035
TO
1024 return $this->controller->get($name);
1025 }
1026
1027 /**
fe482240 1028 * Getter for action.
6a488035
TO
1029 *
1030 * @return int
6a488035 1031 */
00be9182 1032 public function getAction() {
6a488035
TO
1033 return $this->_action;
1034 }
1035
1036 /**
fe482240 1037 * Setter for action.
6a488035 1038 *
6a0b768e
TO
1039 * @param int $action
1040 * The mode we want to set the form.
6a488035 1041 */
00be9182 1042 public function setAction($action) {
6a488035
TO
1043 $this->_action = $action;
1044 }
1045
1046 /**
fe482240 1047 * Assign value to name in template.
6a488035 1048 *
6a0b768e
TO
1049 * @param string $var
1050 * Name of variable.
1051 * @param mixed $value
1052 * Value of variable.
6a488035 1053 */
00be9182 1054 public function assign($var, $value = NULL) {
6a488035
TO
1055 self::$_template->assign($var, $value);
1056 }
1057
1058 /**
fe482240 1059 * Assign value to name in template by reference.
6a488035 1060 *
6a0b768e
TO
1061 * @param string $var
1062 * Name of variable.
1063 * @param mixed $value
8eedd10a 1064 * Value of variable.
6a488035 1065 */
00be9182 1066 public function assign_by_ref($var, &$value) {
6a488035
TO
1067 self::$_template->assign_by_ref($var, $value);
1068 }
1069
4a9538ac 1070 /**
fe482240 1071 * Appends values to template variables.
4a9538ac
CW
1072 *
1073 * @param array|string $tpl_var the template variable name(s)
6a0b768e
TO
1074 * @param mixed $value
1075 * The value to append.
4a9538ac
CW
1076 * @param bool $merge
1077 */
f9f40af3 1078 public function append($tpl_var, $value = NULL, $merge = FALSE) {
4a9538ac
CW
1079 self::$_template->append($tpl_var, $value, $merge);
1080 }
1081
1082 /**
fe482240 1083 * Returns an array containing template variables.
4a9538ac
CW
1084 *
1085 * @param string $name
2a6da8d7 1086 *
4a9538ac
CW
1087 * @return array
1088 */
f9f40af3 1089 public function get_template_vars($name = NULL) {
4a9538ac
CW
1090 return self::$_template->get_template_vars($name);
1091 }
1092
a0ee3941 1093 /**
100fef9d 1094 * @param string $name
a0ee3941
EM
1095 * @param $title
1096 * @param $values
1097 * @param array $attributes
1098 * @param null $separator
1099 * @param bool $required
1100 *
1101 * @return HTML_QuickForm_group
1102 */
00be9182 1103 public function &addRadio($name, $title, $values, $attributes = array(), $separator = NULL, $required = FALSE) {
6a488035 1104 $options = array();
8a4f27dc 1105 $attributes = $attributes ? $attributes : array();
b847e6e7
CW
1106 $allowClear = !empty($attributes['allowClear']);
1107 unset($attributes['allowClear']);
385f11fd 1108 $attributes['id_suffix'] = $name;
6a488035
TO
1109 foreach ($values as $key => $var) {
1110 $options[] = $this->createElement('radio', NULL, NULL, $var, $key, $attributes);
1111 }
1112 $group = $this->addGroup($options, $name, $title, $separator);
3ef93345
MD
1113
1114 $optionEditKey = 'data-option-edit-path';
1115 if (!empty($attributes[$optionEditKey])) {
1116 $group->setAttribute($optionEditKey, $attributes[$optionEditKey]);
1117 }
1118
6a488035
TO
1119 if ($required) {
1120 $this->addRule($name, ts('%1 is a required field.', array(1 => $title)), 'required');
1121 }
b847e6e7
CW
1122 if ($allowClear) {
1123 $group->setAttribute('allowClear', TRUE);
8a4f27dc 1124 }
6a488035
TO
1125 return $group;
1126 }
1127
a0ee3941 1128 /**
100fef9d 1129 * @param int $id
a0ee3941
EM
1130 * @param $title
1131 * @param bool $allowClear
1132 * @param null $required
1133 * @param array $attributes
1134 */
00be9182 1135 public function addYesNo($id, $title, $allowClear = FALSE, $required = NULL, $attributes = array()) {
8a4f27dc 1136 $attributes += array('id_suffix' => $id);
353ffa53 1137 $choice = array();
8a4f27dc
CW
1138 $choice[] = $this->createElement('radio', NULL, '11', ts('Yes'), '1', $attributes);
1139 $choice[] = $this->createElement('radio', NULL, '11', ts('No'), '0', $attributes);
6a488035 1140
8a4f27dc 1141 $group = $this->addGroup($choice, $id, $title);
b847e6e7
CW
1142 if ($allowClear) {
1143 $group->setAttribute('allowClear', TRUE);
8a4f27dc 1144 }
6a488035
TO
1145 if ($required) {
1146 $this->addRule($id, ts('%1 is a required field.', array(1 => $title)), 'required');
1147 }
1148 }
1149
a0ee3941 1150 /**
100fef9d 1151 * @param int $id
a0ee3941
EM
1152 * @param $title
1153 * @param $values
1154 * @param null $other
1155 * @param null $attributes
1156 * @param null $required
1157 * @param null $javascriptMethod
1158 * @param string $separator
1159 * @param bool $flipValues
1160 */
2da40d21 1161 public function addCheckBox(
f9f40af3
TO
1162 $id, $title, $values, $other = NULL,
1163 $attributes = NULL, $required = NULL,
6a488035 1164 $javascriptMethod = NULL,
f9f40af3 1165 $separator = '<br />', $flipValues = FALSE
6a488035
TO
1166 ) {
1167 $options = array();
1168
1169 if ($javascriptMethod) {
1170 foreach ($values as $key => $var) {
1171 if (!$flipValues) {
3ef93345 1172 $options[] = $this->createElement('checkbox', $var, NULL, $key, $javascriptMethod, $attributes);
6a488035
TO
1173 }
1174 else {
3ef93345 1175 $options[] = $this->createElement('checkbox', $key, NULL, $var, $javascriptMethod, $attributes);
6a488035
TO
1176 }
1177 }
1178 }
1179 else {
1180 foreach ($values as $key => $var) {
1181 if (!$flipValues) {
3ef93345 1182 $options[] = $this->createElement('checkbox', $var, NULL, $key, $attributes);
6a488035
TO
1183 }
1184 else {
3ef93345 1185 $options[] = $this->createElement('checkbox', $key, NULL, $var, $attributes);
6a488035
TO
1186 }
1187 }
1188 }
1189
3ef93345
MD
1190 $group = $this->addGroup($options, $id, $title, $separator);
1191 $optionEditKey = 'data-option-edit-path';
1192 if (!empty($attributes[$optionEditKey])) {
1193 $group->setAttribute($optionEditKey, $attributes[$optionEditKey]);
1194 }
6a488035
TO
1195
1196 if ($other) {
1197 $this->addElement('text', $id . '_other', ts('Other'), $attributes[$id . '_other']);
1198 }
1199
1200 if ($required) {
1201 $this->addRule($id,
1202 ts('%1 is a required field.', array(1 => $title)),
1203 'required'
1204 );
1205 }
1206 }
1207
00be9182 1208 public function resetValues() {
6a488035
TO
1209 $data = $this->controller->container();
1210 $data['values'][$this->_name] = array();
1211 }
1212
1213 /**
100fef9d 1214 * Simple shell that derived classes can call to add buttons to
6a488035
TO
1215 * the form with a customized title for the main Submit
1216 *
6a0b768e
TO
1217 * @param string $title
1218 * Title of the main button.
1219 * @param string $nextType
1220 * Button type for the form after processing.
fd31fa4c
EM
1221 * @param string $backType
1222 * @param bool|string $submitOnce If true, add javascript to next button submit which prevents it from being clicked more than once
6a488035 1223 */
00be9182 1224 public function addDefaultButtons($title, $nextType = 'next', $backType = 'back', $submitOnce = FALSE) {
6a488035
TO
1225 $buttons = array();
1226 if ($backType != NULL) {
1227 $buttons[] = array(
1228 'type' => $backType,
1229 'name' => ts('Previous'),
1230 );
1231 }
1232 if ($nextType != NULL) {
1233 $nextButton = array(
1234 'type' => $nextType,
1235 'name' => $title,
1236 'isDefault' => TRUE,
1237 );
1238 if ($submitOnce) {
1239 $nextButton['js'] = array('onclick' => "return submitOnce(this,'{$this->_name}','" . ts('Processing') . "');");
1240 }
1241 $buttons[] = $nextButton;
1242 }
1243 $this->addButtons($buttons);
1244 }
1245
a0ee3941 1246 /**
100fef9d 1247 * @param string $name
a0ee3941
EM
1248 * @param string $from
1249 * @param string $to
1250 * @param string $label
1251 * @param string $dateFormat
1252 * @param bool $required
1253 * @param bool $displayTime
1254 */
00be9182 1255 public function addDateRange($name, $from = '_from', $to = '_to', $label = 'From:', $dateFormat = 'searchDate', $required = FALSE, $displayTime = FALSE) {
6a488035
TO
1256 if ($displayTime) {
1257 $this->addDateTime($name . $from, $label, $required, array('formatType' => $dateFormat));
1258 $this->addDateTime($name . $to, ts('To:'), $required, array('formatType' => $dateFormat));
0db6c3e1
TO
1259 }
1260 else {
6a488035
TO
1261 $this->addDate($name . $from, $label, $required, array('formatType' => $dateFormat));
1262 $this->addDate($name . $to, ts('To:'), $required, array('formatType' => $dateFormat));
1263 }
1264 }
d5965a37 1265
03225ad6
CW
1266 /**
1267 * Based on form action, return a string representing the api action.
1268 * Used by addField method.
1269 *
1270 * Return string
1271 */
1272 private function getApiAction() {
1273 $action = $this->getAction();
1274 if ($action & (CRM_Core_Action::UPDATE + CRM_Core_Action::ADD)) {
1275 return 'create';
1276 }
889dbed8 1277 if ($action & (CRM_Core_Action::VIEW + CRM_Core_Action::BROWSE + CRM_Core_Action::BASIC + CRM_Core_Action::ADVANCED + CRM_Core_Action::PREVIEW)) {
03225ad6
CW
1278 return 'get';
1279 }
1280 // If you get this exception try adding more cases above.
0e02cb01 1281 throw new Exception("Cannot determine api action for " . get_class($this) . '.' . 'CRM_Core_Action "' . CRM_Core_Action::description($action) . '" not recognized.');
03225ad6
CW
1282 }
1283
6e62b28c 1284 /**
d5965a37 1285 * Classes extending CRM_Core_Form should implement this method.
6e62b28c
TM
1286 * @throws Exception
1287 */
1288 public function getDefaultEntity() {
0e02cb01 1289 throw new Exception("Cannot determine default entity. " . get_class($this) . " should implement getDefaultEntity().");
6e62b28c 1290 }
6a488035 1291
1ae720b3
TM
1292 /**
1293 * Classes extending CRM_Core_Form should implement this method.
1294 *
1295 * TODO: Merge with CRM_Core_DAO::buildOptionsContext($context) and add validation.
1296 * @throws Exception
1297 */
1298 public function getDefaultContext() {
0e02cb01 1299 throw new Exception("Cannot determine default context. " . get_class($this) . " should implement getDefaultContext().");
1ae720b3
TM
1300 }
1301
5fafc9b0 1302 /**
fe482240 1303 * Adds a select based on field metadata.
5fafc9b0 1304 * TODO: This could be even more generic and widget type (select in this case) could also be read from metadata
475e9f44 1305 * Perhaps a method like $form->bind($name) which would look up all metadata for named field
6a0b768e
TO
1306 * @param $name
1307 * Field name to go on the form.
1308 * @param array $props
1309 * Mix of html attributes and special properties, namely.
920600e1
CW
1310 * - entity (api entity name, can usually be inferred automatically from the form class)
1311 * - field (field name - only needed if different from name used on the form)
1312 * - option_url - path to edit this option list - usually retrieved automatically - set to NULL to disable link
1313 * - placeholder - set to NULL to disable
d0def949 1314 * - multiple - bool
76773c5a 1315 * - context - @see CRM_Core_DAO::buildOptionsContext
5fafc9b0
CW
1316 * @param bool $required
1317 * @throws CRM_Core_Exception
1318 * @return HTML_QuickForm_Element
1319 */
00be9182 1320 public function addSelect($name, $props = array(), $required = FALSE) {
920600e1 1321 if (!isset($props['entity'])) {
6e62b28c 1322 $props['entity'] = $this->getDefaultEntity();
6a488035 1323 }
920600e1
CW
1324 if (!isset($props['field'])) {
1325 $props['field'] = strrpos($name, '[') ? rtrim(substr($name, 1 + strrpos($name, '[')), ']') : $name;
e869b07d 1326 }
65e8615b
CW
1327 if (!isset($props['context'])) {
1328 try {
1329 $props['context'] = $this->getDefaultContext();
1330 }
1331 // This is not a required param, so we'll ignore if this doesn't exist.
1332 catch (Exception $e) {}
1333 }
f76b27fe
CW
1334 // Fetch options from the api unless passed explicitly
1335 if (isset($props['options'])) {
1336 $options = $props['options'];
1337 }
1338 else {
76352fbc 1339 $info = civicrm_api3($props['entity'], 'getoptions', $props);
f76b27fe
CW
1340 $options = $info['values'];
1341 }
5fafc9b0 1342 if (!array_key_exists('placeholder', $props)) {
76773c5a 1343 $props['placeholder'] = $required ? ts('- select -') : CRM_Utils_Array::value('context', $props) == 'search' ? ts('- any -') : ts('- none -');
5fafc9b0 1344 }
5fafc9b0
CW
1345 // Handle custom field
1346 if (strpos($name, 'custom_') === 0 && is_numeric($name[7])) {
1347 list(, $id) = explode('_', $name);
1348 $label = isset($props['label']) ? $props['label'] : CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomField', 'label', $id);
475e9f44 1349 $gid = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomField', 'option_group_id', $id);
76773c5a
CW
1350 if (CRM_Utils_Array::value('context', $props) != 'search') {
1351 $props['data-option-edit-path'] = array_key_exists('option_url', $props) ? $props['option_url'] : 'civicrm/admin/options/' . CRM_Core_DAO::getFieldValue('CRM_Core_DAO_OptionGroup', $gid);
1352 }
5fafc9b0
CW
1353 }
1354 // Core field
6a488035 1355 else {
f76b27fe 1356 $info = civicrm_api3($props['entity'], 'getfields');
22e263ad 1357 foreach ($info['values'] as $uniqueName => $fieldSpec) {
e869b07d 1358 if (
920600e1
CW
1359 $uniqueName === $props['field'] ||
1360 CRM_Utils_Array::value('name', $fieldSpec) === $props['field'] ||
1361 in_array($props['field'], CRM_Utils_Array::value('api.aliases', $fieldSpec, array()))
e869b07d
CW
1362 ) {
1363 break;
1364 }
6a488035 1365 }
e869b07d 1366 $label = isset($props['label']) ? $props['label'] : $fieldSpec['title'];
76773c5a 1367 if (CRM_Utils_Array::value('context', $props) != 'search') {
599ae208 1368 $props['data-option-edit-path'] = array_key_exists('option_url', $props) ? $props['option_url'] : CRM_Core_PseudoConstant::getOptionEditUrl($fieldSpec);
76773c5a 1369 }
6a488035 1370 }
920600e1
CW
1371 $props['class'] = (isset($props['class']) ? $props['class'] . ' ' : '') . "crm-select2";
1372 $props['data-api-entity'] = $props['entity'];
1373 $props['data-api-field'] = $props['field'];
76773c5a 1374 CRM_Utils_Array::remove($props, 'label', 'entity', 'field', 'option_url', 'options', 'context');
5fafc9b0 1375 return $this->add('select', $name, $label, $options, $required, $props);
6a488035
TO
1376 }
1377
7ec4548b
TM
1378 /**
1379 * Adds a field based on metadata.
1380 *
1381 * @param $name
1382 * Field name to go on the form.
1383 * @param array $props
1384 * Mix of html attributes and special properties, namely.
1385 * - entity (api entity name, can usually be inferred automatically from the form class)
03225ad6 1386 * - name (field name - only needed if different from name used on the form)
7ec4548b
TM
1387 * - option_url - path to edit this option list - usually retrieved automatically - set to NULL to disable link
1388 * - placeholder - set to NULL to disable
1389 * - multiple - bool
1390 * - context - @see CRM_Core_DAO::buildOptionsContext
1391 * @param bool $required
ed0ca248 1392 * @param bool $legacyDate
1393 * Temporary param to facilitate the conversion of fields to use the datepicker in
1394 * a controlled way. To convert the field the jcalendar code needs to be removed from the
1395 * tpl as well. That file is intended to be EOL.
1396 *
03225ad6
CW
1397 * @throws \CiviCRM_API3_Exception
1398 * @throws \Exception
2a300b65 1399 * @return HTML_QuickForm_Element
7ec4548b 1400 */
ed0ca248 1401 public function addField($name, $props = array(), $required = FALSE, $legacyDate = TRUE) {
1ae720b3 1402 // Resolve context.
916b6181 1403 if (empty($props['context'])) {
1ae720b3
TM
1404 $props['context'] = $this->getDefaultContext();
1405 }
916b6181 1406 $context = $props['context'];
7ec4548b 1407 // Resolve entity.
916b6181 1408 if (empty($props['entity'])) {
7ec4548b
TM
1409 $props['entity'] = $this->getDefaultEntity();
1410 }
1411 // Resolve field.
916b6181 1412 if (empty($props['name'])) {
03225ad6 1413 $props['name'] = strrpos($name, '[') ? rtrim(substr($name, 1 + strrpos($name, '[')), ']') : $name;
7ec4548b 1414 }
03225ad6 1415 // Resolve action.
916b6181 1416 if (empty($props['action'])) {
03225ad6 1417 $props['action'] = $this->getApiAction();
7ec4548b 1418 }
2b31bc15
CW
1419
1420 // Handle custom fields
1421 if (strpos($name, 'custom_') === 0 && is_numeric($name[7])) {
1422 $fieldId = (int) substr($name, 7);
916b6181 1423 return CRM_Core_BAO_CustomField::addQuickFormElement($this, $name, $fieldId, $required, $context == 'search', CRM_Utils_Array::value('label', $props));
2b31bc15
CW
1424 }
1425
1426 // Core field - get metadata.
d60a6fba 1427 $fieldSpec = civicrm_api3($props['entity'], 'getfield', $props);
03225ad6 1428 $fieldSpec = $fieldSpec['values'];
80a96508 1429 $fieldSpecLabel = isset($fieldSpec['html']['label']) ? $fieldSpec['html']['label'] : CRM_Utils_Array::value('title', $fieldSpec);
1430 $label = CRM_Utils_Array::value('label', $props, $fieldSpecLabel);
7ec4548b 1431
7ec4548b 1432 $widget = isset($props['type']) ? $props['type'] : $fieldSpec['html']['type'];
916b6181 1433 if ($widget == 'TextArea' && $context == 'search') {
7ec4548b
TM
1434 $widget = 'Text';
1435 }
1436
1437 $isSelect = (in_array($widget, array(
1438 'Select',
dd4706ef
TM
1439 'CheckBoxGroup',
1440 'RadioGroup',
7ec4548b
TM
1441 'Radio',
1442 )));
1443
1444 if ($isSelect) {
2f32ed10 1445 // Fetch options from the api unless passed explicitly.
7ec4548b
TM
1446 if (isset($props['options'])) {
1447 $options = $props['options'];
1448 }
1449 else {
a4969aee 1450 $options = isset($fieldSpec['options']) ? $fieldSpec['options'] : NULL;
7ec4548b 1451 }
916b6181 1452 if ($context == 'search') {
7ec4548b 1453 $widget = 'Select';
65e8615b 1454 $props['multiple'] = CRM_Utils_Array::value('multiple', $props, TRUE);
7ec4548b 1455 }
7ec4548b
TM
1456
1457 // Add data for popup link.
3ef93345
MD
1458 $canEditOptions = CRM_Core_Permission::check('administer CiviCRM');
1459 $hasOptionUrl = !empty($props['option_url']);
1460 $optionUrlKeyIsSet = array_key_exists('option_url', $props);
1461 $shouldAdd = $context !== 'search' && $isSelect && $canEditOptions;
1462
1463 // Only add if key is not set, or if non-empty option url is provided
1464 if (($hasOptionUrl || !$optionUrlKeyIsSet) && $shouldAdd) {
1465 $optionUrl = $hasOptionUrl ? $props['option_url'] :
1466 CRM_Core_PseudoConstant::getOptionEditUrl($fieldSpec);
1467 $props['data-option-edit-path'] = $optionUrl;
7ec4548b 1468 $props['data-api-entity'] = $props['entity'];
03225ad6 1469 $props['data-api-field'] = $props['name'];
7ec4548b
TM
1470 }
1471 }
95457d69 1472 $props += CRM_Utils_Array::value('html', $fieldSpec, array());
65e8615b 1473 CRM_Utils_Array::remove($props, 'entity', 'name', 'context', 'label', 'action', 'type', 'option_url', 'options');
599ae208 1474
b44e3f84 1475 // TODO: refactor switch statement, to separate methods.
7ec4548b
TM
1476 switch ($widget) {
1477 case 'Text':
d8f1758d
CW
1478 case 'Url':
1479 case 'Number':
1480 case 'Email':
7ec4548b 1481 //TODO: Autodetect ranges
5b8080ad 1482 $props['size'] = isset($props['size']) ? $props['size'] : 60;
d8f1758d 1483 return $this->add(strtolower($widget), $name, $label, $props, $required);
7ec4548b 1484
b4b53245 1485 case 'hidden':
2a300b65 1486 return $this->add('hidden', $name, NULL, $props, $required);
b4b53245 1487
0efbca68
TM
1488 case 'TextArea':
1489 //Set default columns and rows for textarea.
1490 $props['rows'] = isset($props['rows']) ? $props['rows'] : 4;
1491 $props['cols'] = isset($props['cols']) ? $props['cols'] : 60;
079f52de 1492 if (empty($props['maxlength']) && isset($fieldSpec['length'])) {
ed71bbca 1493 $props['maxlength'] = $fieldSpec['length'];
1494 }
599ae208 1495 return $this->add('textarea', $name, $label, $props, $required);
0efbca68 1496
db3ec100 1497 case 'Select Date':
ed0ca248 1498 // This is a white list for fields that have been tested with
1499 // date picker. We should be able to remove the other
1500 if ($legacyDate) {
1501 //TODO: add range support
1502 //TODO: Add date formats
1503 //TODO: Add javascript template for dates.
1504 return $this->addDate($name, $label, $required, $props);
1505 }
1506 else {
1507 $fieldSpec = CRM_Utils_Date::addDateMetadataToField($fieldSpec, $fieldSpec);
1508 $attributes = array('format' => $fieldSpec['date_format']);
1509 return $this->add('datepicker', $name, $label, $attributes, $required, $fieldSpec['datepicker']['extra']);
1510 }
db3ec100 1511
a4969aee
TM
1512 case 'Radio':
1513 $separator = isset($props['separator']) ? $props['separator'] : NULL;
125d54e1 1514 unset($props['separator']);
ef3a048a 1515 if (!isset($props['allowClear'])) {
125d54e1 1516 $props['allowClear'] = !$required;
ef3a048a 1517 }
2a300b65 1518 return $this->addRadio($name, $label, $options, $props, $separator, $required);
a4969aee 1519
b248d52b
CW
1520 case 'ChainSelect':
1521 $props += array(
1522 'required' => $required,
1523 'label' => $label,
916b6181 1524 'multiple' => $context == 'search',
b248d52b
CW
1525 );
1526 return $this->addChainSelect($name, $props);
1527
7ec4548b 1528 case 'Select':
b248d52b 1529 $props['class'] = CRM_Utils_Array::value('class', $props, 'big') . ' crm-select2';
65e8615b 1530 if (!array_key_exists('placeholder', $props)) {
78e1efac 1531 $props['placeholder'] = $required ? ts('- select -') : ($context == 'search' ? ts('- any -') : ts('- none -'));
7ec4548b 1532 }
7ec4548b 1533 // TODO: Add and/or option for fields that store multiple values
b2da03d6 1534 return $this->add('select', $name, $label, $options, $required, $props);
7ec4548b 1535
dd4706ef 1536 case 'CheckBoxGroup':
2a300b65 1537 return $this->addCheckBox($name, $label, array_flip($options), $required, $props);
dd4706ef
TM
1538
1539 case 'RadioGroup':
2a300b65 1540 return $this->addRadio($name, $label, $options, $props, NULL, $required);
dd4706ef 1541
a4969aee 1542 case 'CheckBox':
999ab5e1
TM
1543 $text = isset($props['text']) ? $props['text'] : NULL;
1544 unset($props['text']);
2a300b65 1545 return $this->addElement('checkbox', $name, $label, $text, $props);
a4969aee 1546
50471995 1547 //add support for 'Advcheckbox' field
1548 case 'advcheckbox':
b0964781 1549 $text = isset($props['text']) ? $props['text'] : NULL;
1550 unset($props['text']);
1551 return $this->addElement('advcheckbox', $name, $label, $text, $props);
50471995 1552
33fa033c
TM
1553 case 'File':
1554 // We should not build upload file in search mode.
916b6181 1555 if ($context == 'search') {
33fa033c
TM
1556 return;
1557 }
2a300b65 1558 $file = $this->add('file', $name, $label, $props, $required);
33fa033c 1559 $this->addUploadElement($name);
2a300b65 1560 return $file;
33fa033c 1561
b66c1d2c
CW
1562 case 'RichTextEditor':
1563 return $this->add('wysiwyg', $name, $label, $props, $required);
1564
b58770ea 1565 case 'EntityRef':
2a300b65 1566 return $this->addEntityRef($name, $label, $props, $required);
b58770ea 1567
e9bc5dcc
SL
1568 case 'Password':
1569 return $this->add('password', $name, $label, $props, $required);
1570
7ec4548b
TM
1571 // Check datatypes of fields
1572 // case 'Int':
1573 //case 'Float':
1574 //case 'Money':
7ec4548b
TM
1575 //case read only fields
1576 default:
1577 throw new Exception("Unsupported html-element " . $widget);
1578 }
1579 }
1580
6a488035
TO
1581 /**
1582 * Add a widget for selecting/editing/creating/copying a profile form
1583 *
6a0b768e
TO
1584 * @param string $name
1585 * HTML form-element name.
1586 * @param string $label
1587 * Printable label.
1588 * @param string $allowCoreTypes
1589 * Only present a UFGroup if its group_type includes a subset of $allowCoreTypes; e.g. 'Individual', 'Activity'.
1590 * @param string $allowSubTypes
1591 * Only present a UFGroup if its group_type is compatible with $allowSubypes.
6a488035 1592 * @param array $entities
6a0b768e
TO
1593 * @param bool $default
1594 * //CRM-15427.
54957108 1595 * @param string $usedFor
6a488035 1596 */
37375016 1597 public function addProfileSelector($name, $label, $allowCoreTypes, $allowSubTypes, $entities, $default = FALSE, $usedFor = NULL) {
6a488035
TO
1598 // Output widget
1599 // FIXME: Instead of adhoc serialization, use a single json_encode()
1600 CRM_UF_Page_ProfileEditor::registerProfileScripts();
1601 CRM_UF_Page_ProfileEditor::registerSchemas(CRM_Utils_Array::collect('entity_type', $entities));
1602 $this->add('text', $name, $label, array(
1603 'class' => 'crm-profile-selector',
1604 // Note: client treats ';;' as equivalent to \0, and ';;' works better in HTML
1605 'data-group-type' => CRM_Core_BAO_UFGroup::encodeGroupType($allowCoreTypes, $allowSubTypes, ';;'),
1606 'data-entities' => json_encode($entities),
99e239bc 1607 //CRM-15427
1608 'data-default' => $default,
37375016 1609 'data-usedfor' => json_encode($usedFor),
6a488035
TO
1610 ));
1611 }
1612
a0ee3941
EM
1613 /**
1614 * @return null
1615 */
6a488035
TO
1616 public function getRootTitle() {
1617 return NULL;
1618 }
1619
a0ee3941
EM
1620 /**
1621 * @return string
1622 */
6a488035
TO
1623 public function getCompleteTitle() {
1624 return $this->getRootTitle() . $this->getTitle();
1625 }
1626
a0ee3941
EM
1627 /**
1628 * @return CRM_Core_Smarty
1629 */
00be9182 1630 public static function &getTemplate() {
6a488035
TO
1631 return self::$_template;
1632 }
1633
a0ee3941
EM
1634 /**
1635 * @param $elementName
1636 */
00be9182 1637 public function addUploadElement($elementName) {
6a488035
TO
1638 $uploadNames = $this->get('uploadNames');
1639 if (!$uploadNames) {
1640 $uploadNames = array();
1641 }
1642 if (is_array($elementName)) {
1643 foreach ($elementName as $name) {
1644 if (!in_array($name, $uploadNames)) {
1645 $uploadNames[] = $name;
1646 }
1647 }
1648 }
1649 else {
1650 if (!in_array($elementName, $uploadNames)) {
1651 $uploadNames[] = $elementName;
1652 }
1653 }
1654 $this->set('uploadNames', $uploadNames);
1655
1656 $config = CRM_Core_Config::singleton();
1657 if (!empty($uploadNames)) {
1658 $this->controller->addUploadAction($config->customFileUploadDir, $uploadNames);
1659 }
1660 }
1661
a0ee3941
EM
1662 /**
1663 * @param $name
1664 *
1665 * @return null
1666 */
00be9182 1667 public function getVar($name) {
6a488035
TO
1668 return isset($this->$name) ? $this->$name : NULL;
1669 }
1670
a0ee3941
EM
1671 /**
1672 * @param $name
1673 * @param $value
1674 */
00be9182 1675 public function setVar($name, $value) {
6a488035
TO
1676 $this->$name = $value;
1677 }
1678
1679 /**
fe482240 1680 * Add date.
6a488035 1681 *
013ac5df
CW
1682 * @deprecated
1683 * Use $this->add('datepicker', ...) instead.
a1a2a83d
TO
1684 *
1685 * @param string $name
1686 * Name of the element.
1687 * @param string $label
1688 * Label of the element.
6a0b768e
TO
1689 * @param bool $required
1690 * True if required.
a1a2a83d
TO
1691 * @param array $attributes
1692 * Key / value pair.
6a488035 1693 */
00be9182 1694 public function addDate($name, $label, $required = FALSE, $attributes = NULL) {
a7488080 1695 if (!empty($attributes['formatType'])) {
6a488035
TO
1696 // get actual format
1697 $params = array('name' => $attributes['formatType']);
1698 $values = array();
1699
1700 // cache date information
1701 static $dateFormat;
1702 $key = "dateFormat_" . str_replace(' ', '_', $attributes['formatType']);
a7488080 1703 if (empty($dateFormat[$key])) {
6a488035
TO
1704 CRM_Core_DAO::commonRetrieve('CRM_Core_DAO_PreferencesDate', $params, $values);
1705 $dateFormat[$key] = $values;
1706 }
1707 else {
1708 $values = $dateFormat[$key];
1709 }
1710
1711 if ($values['date_format']) {
1712 $attributes['format'] = $values['date_format'];
1713 }
1714
a7488080 1715 if (!empty($values['time_format'])) {
6a488035
TO
1716 $attributes['timeFormat'] = $values['time_format'];
1717 }
1718 $attributes['startOffset'] = $values['start'];
1719 $attributes['endOffset'] = $values['end'];
1720 }
1721
1722 $config = CRM_Core_Config::singleton();
a7488080 1723 if (empty($attributes['format'])) {
6a488035
TO
1724 $attributes['format'] = $config->dateInputFormat;
1725 }
1726
1727 if (!isset($attributes['startOffset'])) {
1728 $attributes['startOffset'] = 10;
1729 }
1730
1731 if (!isset($attributes['endOffset'])) {
1732 $attributes['endOffset'] = 10;
1733 }
1734
1735 $this->add('text', $name, $label, $attributes);
1736
8cc574cf 1737 if (!empty($attributes['addTime']) || !empty($attributes['timeFormat'])) {
6a488035
TO
1738
1739 if (!isset($attributes['timeFormat'])) {
1740 $timeFormat = $config->timeInputFormat;
1741 }
1742 else {
1743 $timeFormat = $attributes['timeFormat'];
1744 }
1745
1746 // 1 - 12 hours and 2 - 24 hours, but for jquery widget it is 0 and 1 respectively
1747 if ($timeFormat) {
1748 $show24Hours = TRUE;
1749 if ($timeFormat == 1) {
1750 $show24Hours = FALSE;
1751 }
1752
1753 //CRM-6664 -we are having time element name
1754 //in either flat string or an array format.
1755 $elementName = $name . '_time';
1756 if (substr($name, -1) == ']') {
1757 $elementName = substr($name, 0, strlen($name) - 1) . '_time]';
1758 }
1759
1760 $this->add('text', $elementName, ts('Time'), array('timeFormat' => $show24Hours));
1761 }
1762 }
1763
1764 if ($required) {
1765 $this->addRule($name, ts('Please select %1', array(1 => $label)), 'required');
8cc574cf 1766 if (!empty($attributes['addTime']) && !empty($attributes['addTimeRequired'])) {
6a488035
TO
1767 $this->addRule($elementName, ts('Please enter a time.'), 'required');
1768 }
1769 }
1770 }
1771
1772 /**
013ac5df
CW
1773 * Function that will add date and time.
1774 *
1775 * @deprecated
1776 * Use $this->add('datepicker', ...) instead.
54957108 1777 *
1778 * @param string $name
1779 * @param string $label
1780 * @param bool $required
1781 * @param null $attributes
6a488035 1782 */
00be9182 1783 public function addDateTime($name, $label, $required = FALSE, $attributes = NULL) {
6a488035
TO
1784 $addTime = array('addTime' => TRUE);
1785 if (is_array($attributes)) {
1786 $attributes = array_merge($attributes, $addTime);
1787 }
1788 else {
1789 $attributes = $addTime;
1790 }
1791
1792 $this->addDate($name, $label, $required, $attributes);
1793 }
1794
1795 /**
fe482240 1796 * Add a currency and money element to the form.
3bdf1f3a 1797 *
1798 * @param string $name
1799 * @param string $label
1800 * @param bool $required
1801 * @param null $attributes
1802 * @param bool $addCurrency
1803 * @param string $currencyName
1804 * @param null $defaultCurrency
1805 * @param bool $freezeCurrency
1806 *
1807 * @return \HTML_QuickForm_Element
6a488035 1808 */
2da40d21 1809 public function addMoney(
f9f40af3 1810 $name,
6a488035 1811 $label,
f9f40af3
TO
1812 $required = FALSE,
1813 $attributes = NULL,
1814 $addCurrency = TRUE,
1815 $currencyName = 'currency',
6a488035 1816 $defaultCurrency = NULL,
f9f40af3 1817 $freezeCurrency = FALSE
6a488035
TO
1818 ) {
1819 $element = $this->add('text', $name, $label, $attributes, $required);
1820 $this->addRule($name, ts('Please enter a valid amount.'), 'money');
1821
1822 if ($addCurrency) {
1823 $ele = $this->addCurrency($currencyName, NULL, TRUE, $defaultCurrency, $freezeCurrency);
1824 }
1825
1826 return $element;
1827 }
1828
1829 /**
fe482240 1830 * Add currency element to the form.
54957108 1831 *
1832 * @param string $name
1833 * @param null $label
1834 * @param bool $required
1835 * @param string $defaultCurrency
1836 * @param bool $freezeCurrency
483a53a8 1837 * @param bool $setDefaultCurrency
6a488035 1838 */
2da40d21 1839 public function addCurrency(
f9f40af3
TO
1840 $name = 'currency',
1841 $label = NULL,
1842 $required = TRUE,
6a488035 1843 $defaultCurrency = NULL,
483a53a8 1844 $freezeCurrency = FALSE,
1845 $setDefaultCurrency = TRUE
6a488035
TO
1846 ) {
1847 $currencies = CRM_Core_OptionGroup::values('currencies_enabled');
b740ee4b
MW
1848 if (!array_key_exists($defaultCurrency, $currencies)) {
1849 Civi::log()->warning('addCurrency: Currency ' . $defaultCurrency . ' is disabled but still in use!');
1850 $currencies[$defaultCurrency] = $defaultCurrency;
1851 }
e1462487 1852 $options = array('class' => 'crm-select2 eight');
6a488035 1853 if (!$required) {
e1462487
CW
1854 $currencies = array('' => '') + $currencies;
1855 $options['placeholder'] = ts('- none -');
6a488035 1856 }
e1462487 1857 $ele = $this->add('select', $name, $label, $currencies, $required, $options);
6a488035
TO
1858 if ($freezeCurrency) {
1859 $ele->freeze();
1860 }
1861 if (!$defaultCurrency) {
1862 $config = CRM_Core_Config::singleton();
1863 $defaultCurrency = $config->defaultCurrency;
1864 }
483a53a8 1865 // In some case, setting currency field by default might override the default value
1866 // as encountered in CRM-20527 for batch data entry
1867 if ($setDefaultCurrency) {
1868 $this->setDefaults(array($name => $defaultCurrency));
1869 }
6a488035
TO
1870 }
1871
47f21f3a 1872 /**
fe482240 1873 * Create a single or multiple entity ref field.
47f21f3a
CW
1874 * @param string $name
1875 * @param string $label
6a0b768e
TO
1876 * @param array $props
1877 * Mix of html and widget properties, including:.
16b10e64
CW
1878 * - select - params to give to select2 widget
1879 * - entity - defaults to contact
1880 * - create - can the user create a new entity on-the-fly?
79ae07d9 1881 * Set to TRUE if entity is contact and you want the default profiles,
b9aa8f56 1882 * or pass in your own set of links. @see CRM_Core_BAO_UFGroup::getCreateLinks for format
353ea873 1883 * note that permissions are checked automatically
16b10e64 1884 * - api - array of settings for the getlist api wrapper
353ea873 1885 * note that it accepts a 'params' setting which will be passed to the underlying api
16b10e64
CW
1886 * - placeholder - string
1887 * - multiple - bool
1888 * - class, etc. - other html properties
fd36866a 1889 * @param bool $required
79ae07d9 1890 *
47f21f3a
CW
1891 * @return HTML_QuickForm_Element
1892 */
00be9182 1893 public function addEntityRef($name, $label = '', $props = array(), $required = FALSE) {
a88cf11a 1894 require_once "api/api.php";
c66581f5 1895 $config = CRM_Core_Config::singleton();
76ec9ca7 1896 // Default properties
704d3260 1897 $props['api'] = CRM_Utils_Array::value('api', $props, array());
a88cf11a
CW
1898 $props['entity'] = _civicrm_api_get_entity_name_from_camel(CRM_Utils_Array::value('entity', $props, 'contact'));
1899 $props['class'] = ltrim(CRM_Utils_Array::value('class', $props, '') . ' crm-form-entityref');
47f21f3a 1900
79ae07d9
CW
1901 if ($props['entity'] == 'contact' && isset($props['create']) && !(CRM_Core_Permission::check('edit all contacts') || CRM_Core_Permission::check('add contacts'))) {
1902 unset($props['create']);
1903 }
79ae07d9 1904
a88cf11a
CW
1905 $props['placeholder'] = CRM_Utils_Array::value('placeholder', $props, $required ? ts('- select %1 -', array(1 => ts(str_replace('_', ' ', $props['entity'])))) : ts('- none -'));
1906
1907 $defaults = array();
1908 if (!empty($props['multiple'])) {
1909 $defaults['multiple'] = TRUE;
79ae07d9
CW
1910 }
1911 $props['select'] = CRM_Utils_Array::value('select', $props, array()) + $defaults;
47f21f3a 1912
f9585de5 1913 $this->formatReferenceFieldAttributes($props, get_class($this));
47f21f3a
CW
1914 return $this->add('text', $name, $label, $props, $required);
1915 }
1916
1917 /**
f9585de5 1918 * @param array $props
1919 * @param string $formName
47f21f3a 1920 */
f9585de5 1921 private function formatReferenceFieldAttributes(&$props, $formName) {
1922 CRM_Utils_Hook::alterEntityRefParams($props, $formName);
47f21f3a 1923 $props['data-select-params'] = json_encode($props['select']);
76ec9ca7
CW
1924 $props['data-api-params'] = $props['api'] ? json_encode($props['api']) : NULL;
1925 $props['data-api-entity'] = $props['entity'];
79ae07d9
CW
1926 if (!empty($props['create'])) {
1927 $props['data-create-links'] = json_encode($props['create']);
47f21f3a 1928 }
a88cf11a 1929 CRM_Utils_Array::remove($props, 'multiple', 'select', 'api', 'entity', 'create');
47f21f3a
CW
1930 }
1931
5d86176b 1932 /**
1933 * Convert all date fields within the params to mysql date ready for the
1934 * BAO layer. In this case fields are checked against the $_datefields defined for the form
1935 * and if time is defined it is incorporated
1936 *
6a0b768e
TO
1937 * @param array $params
1938 * Input params from the form.
5d86176b 1939 *
1940 * @todo it would probably be better to work on $this->_params than a passed array
1941 * @todo standardise the format which dates are passed to the BAO layer in & remove date
1942 * handling from BAO
1943 */
9b873358
TO
1944 public function convertDateFieldsToMySQL(&$params) {
1945 foreach ($this->_dateFields as $fieldName => $specs) {
1946 if (!empty($params[$fieldName])) {
5d86176b 1947 $params[$fieldName] = CRM_Utils_Date::isoToMysql(
1948 CRM_Utils_Date::processDate(
353ffa53
TO
1949 $params[$fieldName],
1950 CRM_Utils_Array::value("{$fieldName}_time", $params), TRUE)
5d86176b 1951 );
1952 }
92e4c2a5 1953 else {
9b873358 1954 if (isset($specs['default'])) {
5d86176b 1955 $params[$fieldName] = date('YmdHis', strtotime($specs['default']));
1956 }
1957 }
1958 }
1959 }
1960
a0ee3941
EM
1961 /**
1962 * @param $elementName
1963 */
00be9182 1964 public function removeFileRequiredRules($elementName) {
6a488035
TO
1965 $this->_required = array_diff($this->_required, array($elementName));
1966 if (isset($this->_rules[$elementName])) {
1967 foreach ($this->_rules[$elementName] as $index => $ruleInfo) {
1968 if ($ruleInfo['type'] == 'uploadedfile') {
1969 unset($this->_rules[$elementName][$index]);
1970 }
1971 }
1972 if (empty($this->_rules[$elementName])) {
1973 unset($this->_rules[$elementName]);
1974 }
1975 }
1976 }
1977
1978 /**
fe482240 1979 * Function that can be defined in Form to override or.
6a488035 1980 * perform specific action on cancel action
6a488035 1981 */
f9f40af3
TO
1982 public function cancelAction() {
1983 }
7cb3d4f0
CW
1984
1985 /**
fe482240 1986 * Helper function to verify that required fields have been filled.
3bdf1f3a 1987 *
7cb3d4f0 1988 * Typically called within the scope of a FormRule function
3bdf1f3a 1989 *
1990 * @param array $fields
1991 * @param array $values
1992 * @param array $errors
7cb3d4f0 1993 */
00be9182 1994 public static function validateMandatoryFields($fields, $values, &$errors) {
7cb3d4f0
CW
1995 foreach ($fields as $name => $fld) {
1996 if (!empty($fld['is_required']) && CRM_Utils_System::isNull(CRM_Utils_Array::value($name, $values))) {
1997 $errors[$name] = ts('%1 is a required field.', array(1 => $fld['title']));
1998 }
1999 }
2000 }
da8d9879 2001
aa1b1481
EM
2002 /**
2003 * Get contact if for a form object. Prioritise
16b10e64 2004 * - cid in URL if 0 (on behalf on someoneelse)
aa1b1481 2005 * (@todo consider setting a variable if onbehalf for clarity of downstream 'if's
16b10e64
CW
2006 * - logged in user id if it matches the one in the cid in the URL
2007 * - contact id validated from a checksum from a checksum
2008 * - cid from the url if the caller has ACL permission to view
2009 * - fallback is logged in user (or ? NULL if no logged in user) (@todo wouldn't 0 be more intuitive?)
aa1b1481 2010 *
5c766a0b 2011 * @return NULL|int
aa1b1481 2012 */
8d388047 2013 protected function setContactID() {
da8d9879 2014 $tempID = CRM_Utils_Request::retrieve('cid', 'Positive', $this);
7b4d7ab8 2015 if (isset($this->_params) && !empty($this->_params['select_contact_id'])) {
596bff78 2016 $tempID = $this->_params['select_contact_id'];
2017 }
22e263ad 2018 if (isset($this->_params, $this->_params[0]) && !empty($this->_params[0]['select_contact_id'])) {
e1ce628e 2019 // event form stores as an indexed array, contribution form not so much...
2020 $tempID = $this->_params[0]['select_contact_id'];
2021 }
c156d4d6 2022
da8d9879 2023 // force to ignore the authenticated user
c156d4d6
E
2024 if ($tempID === '0' || $tempID === 0) {
2025 // we set the cid on the form so that this will be retained for the Confirm page
2026 // in the multi-page form & prevent us returning the $userID when this is called
2027 // from that page
2028 // we don't really need to set it when $tempID is set because the params have that stored
2029 $this->set('cid', 0);
11cf613f 2030 CRM_Core_Resources::singleton()->addVars('coreForm', array('contact_id' => (int) $tempID));
aa288d3f 2031 return (int) $tempID;
da8d9879
DG
2032 }
2033
596bff78 2034 $userID = $this->getLoggedInUserContactID();
da8d9879 2035
18406494 2036 if (!is_null($tempID) && $tempID === $userID) {
11cf613f 2037 CRM_Core_Resources::singleton()->addVars('coreForm', array('contact_id' => (int) $tempID));
aa288d3f 2038 return (int) $userID;
da8d9879
DG
2039 }
2040
2041 //check if this is a checksum authentication
2042 $userChecksum = CRM_Utils_Request::retrieve('cs', 'String', $this);
2043 if ($userChecksum) {
2044 //check for anonymous user.
2045 $validUser = CRM_Contact_BAO_Contact_Utils::validChecksum($tempID, $userChecksum);
2046 if ($validUser) {
11cf613f 2047 CRM_Core_Resources::singleton()->addVars('coreForm', array('contact_id' => (int) $tempID));
2048 CRM_Core_Resources::singleton()->addVars('coreForm', array('checksum' => (int) $tempID));
da8d9879
DG
2049 return $tempID;
2050 }
2051 }
2052 // check if user has permission, CRM-12062
4c9b6178 2053 elseif ($tempID && CRM_Contact_BAO_Contact_Permission::allow($tempID)) {
11cf613f 2054 CRM_Core_Resources::singleton()->addVars('coreForm', array('contact_id' => (int) $tempID));
da8d9879
DG
2055 return $tempID;
2056 }
064af727 2057 if (is_numeric($userID)) {
2058 CRM_Core_Resources::singleton()->addVars('coreForm', array('contact_id' => (int) $userID));
2059 }
f03d4901 2060 return is_numeric($userID) ? $userID : NULL;
da8d9879 2061 }
596bff78 2062
3bdf1f3a 2063 /**
2064 * Get the contact id that the form is being submitted for.
2065 *
2066 * @return int|NULL
2067 */
8d388047 2068 public function getContactID() {
2069 return $this->setContactID();
2070 }
2071
f9f40af3 2072 /**
fe482240 2073 * Get the contact id of the logged in user.
f9f40af3 2074 */
00be9182 2075 public function getLoggedInUserContactID() {
596bff78 2076 // check if the user is logged in and has a contact ID
2077 $session = CRM_Core_Session::singleton();
2078 return $session->get('userID');
2079 }
2080
2081 /**
100fef9d 2082 * Add autoselector field -if user has permission to view contacts
596bff78 2083 * If adding this to a form you also need to add to the tpl e.g
2084 *
2085 * {if !empty($selectable)}
2086 * <div class="crm-summary-row">
2087 * <div class="crm-label">{$form.select_contact.label}</div>
2088 * <div class="crm-content">
2089 * {$form.select_contact.html}
2090 * </div>
2091 * </div>
2092 * {/if}
77b97be7 2093 *
6a0b768e
TO
2094 * @param array $profiles
2095 * Ids of profiles that are on the form (to be autofilled).
77b97be7
EM
2096 * @param array $autoCompleteField
2097 *
16b10e64
CW
2098 * - name_field
2099 * - id_field
2100 * - url (for ajax lookup)
596bff78 2101 *
77b97be7 2102 * @todo add data attributes so we can deal with multiple instances on a form
596bff78 2103 */
00be9182 2104 public function addAutoSelector($profiles = array(), $autoCompleteField = array()) {
596bff78 2105 $autoCompleteField = array_merge(array(
353ffa53
TO
2106 'id_field' => 'select_contact_id',
2107 'placeholder' => ts('Select someone else ...'),
2108 'show_hide' => TRUE,
2109 'api' => array('params' => array('contact_type' => 'Individual')),
2110 ), $autoCompleteField);
596bff78 2111
22e263ad 2112 if ($this->canUseAjaxContactLookups()) {
25977d86 2113 $this->assign('selectable', $autoCompleteField['id_field']);
353ffa53
TO
2114 $this->addEntityRef($autoCompleteField['id_field'], NULL, array(
2115 'placeholder' => $autoCompleteField['placeholder'],
af9b09df 2116 'api' => $autoCompleteField['api'],
353ffa53 2117 ));
596bff78 2118
96ed17aa 2119 CRM_Core_Resources::singleton()->addScriptFile('civicrm', 'js/AlternateContactSelector.js', 1, 'html-header')
353ffa53
TO
2120 ->addSetting(array(
2121 'form' => array('autocompletes' => $autoCompleteField),
2122 'ids' => array('profile' => $profiles),
2123 ));
596bff78 2124 }
2125 }
2126
dc677c00 2127 /**
dc677c00 2128 */
00be9182 2129 public function canUseAjaxContactLookups() {
dc677c00 2130 if (0 < (civicrm_api3('contact', 'getcount', array('check_permissions' => 1))) &&
353ffa53
TO
2131 CRM_Core_Permission::check(array(array('access AJAX API', 'access CiviCRM')))
2132 ) {
f9f40af3
TO
2133 return TRUE;
2134 }
dc677c00
EM
2135 }
2136
596bff78 2137 /**
2138 * Add the options appropriate to cid = zero - ie. autocomplete
2139 *
2140 * @todo there is considerable code duplication between the contribution forms & event forms. It is apparent
2141 * that small pieces of duplication are not being refactored into separate functions because their only shared parent
2142 * is this form. Inserting a class FrontEndForm.php between the contribution & event & this class would allow functions like this
2143 * and a dozen other small ones to be refactored into a shared parent with the reduction of much code duplication
7a9ab499
EM
2144 *
2145 * @param $onlinePaymentProcessorEnabled
596bff78 2146 */
00be9182 2147 public function addCIDZeroOptions($onlinePaymentProcessorEnabled) {
596bff78 2148 $this->assign('nocid', TRUE);
2149 $profiles = array();
22e263ad 2150 if ($this->_values['custom_pre_id']) {
596bff78 2151 $profiles[] = $this->_values['custom_pre_id'];
2152 }
22e263ad 2153 if ($this->_values['custom_post_id']) {
cc57909a 2154 $profiles = array_merge($profiles, (array) $this->_values['custom_post_id']);
596bff78 2155 }
22e263ad 2156 if ($onlinePaymentProcessorEnabled) {
596bff78 2157 $profiles[] = 'billing';
2158 }
22e263ad 2159 if (!empty($this->_values)) {
596bff78 2160 $this->addAutoSelector($profiles);
2161 }
2162 }
9d665938 2163
2164 /**
2165 * Set default values on form for given contact (or no contact defaults)
77b97be7 2166 *
6a0b768e
TO
2167 * @param mixed $profile_id
2168 * (can be id, or profile name).
2169 * @param int $contactID
77b97be7
EM
2170 *
2171 * @return array
9d665938 2172 */
00be9182 2173 public function getProfileDefaults($profile_id = 'Billing', $contactID = NULL) {
92e4c2a5 2174 try {
9d665938 2175 $defaults = civicrm_api3('profile', 'getsingle', array(
2176 'profile_id' => (array) $profile_id,
2177 'contact_id' => $contactID,
2178 ));
2179 return $defaults;
2180 }
2181 catch (Exception $e) {
9d665938 2182 // the try catch block gives us silent failure -not 100% sure this is a good idea
2183 // as silent failures are often worse than noisy ones
2ab5ff1d 2184 return array();
9d665938 2185 }
2186 }
cae80d9f
CW
2187
2188 /**
fe482240 2189 * Sets form attribute.
cae80d9f
CW
2190 * @see CRM.loadForm
2191 */
00be9182 2192 public function preventAjaxSubmit() {
cae80d9f
CW
2193 $this->setAttribute('data-no-ajax-submit', 'true');
2194 }
2195
2196 /**
fe482240 2197 * Sets form attribute.
cae80d9f
CW
2198 * @see CRM.loadForm
2199 */
00be9182 2200 public function allowAjaxSubmit() {
cae80d9f
CW
2201 $this->removeAttribute('data-no-ajax-submit');
2202 }
e2046b33
CW
2203
2204 /**
fe482240 2205 * Sets page title based on entity and action.
e2046b33
CW
2206 * @param string $entityLabel
2207 */
00be9182 2208 public function setPageTitle($entityLabel) {
e2046b33
CW
2209 switch ($this->_action) {
2210 case CRM_Core_Action::ADD:
2211 CRM_Utils_System::setTitle(ts('New %1', array(1 => $entityLabel)));
2212 break;
f9f40af3 2213
e2046b33
CW
2214 case CRM_Core_Action::UPDATE:
2215 CRM_Utils_System::setTitle(ts('Edit %1', array(1 => $entityLabel)));
2216 break;
f9f40af3 2217
e2046b33
CW
2218 case CRM_Core_Action::VIEW:
2219 case CRM_Core_Action::PREVIEW:
2220 CRM_Utils_System::setTitle(ts('View %1', array(1 => $entityLabel)));
2221 break;
f9f40af3 2222
e2046b33
CW
2223 case CRM_Core_Action::DELETE:
2224 CRM_Utils_System::setTitle(ts('Delete %1', array(1 => $entityLabel)));
2225 break;
2226 }
2227 }
1d07e7ab
CW
2228
2229 /**
2230 * Create a chain-select target field. All settings are optional; the defaults usually work.
2231 *
2232 * @param string $elementName
2233 * @param array $settings
2234 *
2235 * @return HTML_QuickForm_Element
2236 */
2237 public function addChainSelect($elementName, $settings = array()) {
2238 $props = $settings += array(
353ffa53
TO
2239 'control_field' => str_replace(array('state_province', 'StateProvince', 'county', 'County'), array(
2240 'country',
2241 'Country',
2242 'state_province',
af9b09df 2243 'StateProvince',
353ffa53 2244 ), $elementName),
1d07e7ab 2245 'data-callback' => strpos($elementName, 'rovince') ? 'civicrm/ajax/jqState' : 'civicrm/ajax/jqCounty',
757069de 2246 'label' => strpos($elementName, 'rovince') ? ts('State/Province') : ts('County'),
1d07e7ab
CW
2247 'data-empty-prompt' => strpos($elementName, 'rovince') ? ts('Choose country first') : ts('Choose state first'),
2248 'data-none-prompt' => ts('- N/A -'),
2249 'multiple' => FALSE,
2250 'required' => FALSE,
2251 'placeholder' => empty($settings['required']) ? ts('- none -') : ts('- select -'),
2252 );
b248d52b 2253 CRM_Utils_Array::remove($props, 'label', 'required', 'control_field', 'context');
8f9c3cbe 2254 $props['class'] = (empty($props['class']) ? '' : "{$props['class']} ") . 'crm-select2';
1d07e7ab
CW
2255 $props['data-select-prompt'] = $props['placeholder'];
2256 $props['data-name'] = $elementName;
2257
2258 $this->_chainSelectFields[$settings['control_field']] = $elementName;
2259
6a6ab43a
CW
2260 // Passing NULL instead of an array of options
2261 // CRM-15225 - normally QF will reject any selected values that are not part of the field's options, but due to a
2262 // quirk in our patched version of HTML_QuickForm_select, this doesn't happen if the options are NULL
2263 // which seems a bit dirty but it allows our dynamically-popuplated select element to function as expected.
c46f87cf 2264 return $this->add('select', $elementName, $settings['label'], NULL, $settings['required'], $props);
1d07e7ab
CW
2265 }
2266
87ecd5b7 2267 /**
2268 * Add actions menu to results form.
2269 *
c794f667 2270 * @param array $tasks
87ecd5b7 2271 */
2272 public function addTaskMenu($tasks) {
2273 if (is_array($tasks) && !empty($tasks)) {
1a7356e7 2274 // Set constants means this will always load with an empty value, not reloading any submitted value.
2275 // This is appropriate as it is a pseudofield.
2276 $this->setConstants(array('task' => ''));
44543184 2277 $this->assign('taskMetaData', $tasks);
2278 $select = $this->add('select', 'task', NULL, array('' => ts('Actions')), FALSE, array(
2279 'class' => 'crm-select2 crm-action-menu fa-check-circle-o huge crm-search-result-actions')
2280 );
2281 foreach ($tasks as $key => $task) {
2282 $attributes = array();
1a7356e7 2283 if (isset($task['data'])) {
2284 foreach ($task['data'] as $dataKey => $dataValue) {
2285 $attributes['data-' . $dataKey] = $dataValue;
2286 }
44543184 2287 }
2288 $select->addOption($task['title'], $key, $attributes);
2289 }
87ecd5b7 2290 if (empty($this->_actionButtonName)) {
2291 $this->_actionButtonName = $this->getButtonName('next', 'action');
2292 }
2293 $this->assign('actionButtonName', $this->_actionButtonName);
2294 $this->add('submit', $this->_actionButtonName, ts('Go'), array('class' => 'hiddenElement crm-search-go-button'));
2295
2296 // Radio to choose "All items" or "Selected items only"
2297 $selectedRowsRadio = $this->addElement('radio', 'radio_ts', NULL, '', 'ts_sel', array('checked' => 'checked'));
2298 $allRowsRadio = $this->addElement('radio', 'radio_ts', NULL, '', 'ts_all');
2299 $this->assign('ts_sel_id', $selectedRowsRadio->_attributes['id']);
2300 $this->assign('ts_all_id', $allRowsRadio->_attributes['id']);
2301
2302 CRM_Core_Resources::singleton()->addScriptFile('civicrm', 'js/crm.searchForm.js', 1, 'html-header');
2303 }
2304 }
2305
1d07e7ab
CW
2306 /**
2307 * Set options and attributes for chain select fields based on the controlling field's value
2308 */
2309 private function preProcessChainSelectFields() {
2310 foreach ($this->_chainSelectFields as $control => $target) {
a3984622
OB
2311 // The 'target' might get missing if extensions do removeElement() in a form hook.
2312 if ($this->elementExists($target)) {
2313 $targetField = $this->getElement($target);
2314 $targetType = $targetField->getAttribute('data-callback') == 'civicrm/ajax/jqCounty' ? 'county' : 'stateProvince';
2315 $options = array();
2316 // If the control field is on the form, setup chain-select and dynamically populate options
2317 if ($this->elementExists($control)) {
2318 $controlField = $this->getElement($control);
2319 $controlType = $targetType == 'county' ? 'stateProvince' : 'country';
2320
2321 $targetField->setAttribute('class', $targetField->getAttribute('class') . ' crm-chain-select-target');
2322
2323 $css = (string) $controlField->getAttribute('class');
2324 $controlField->updateAttributes(array(
2325 'class' => ($css ? "$css " : 'crm-select2 ') . 'crm-chain-select-control',
2326 'data-target' => $target,
2327 ));
2328 $controlValue = $controlField->getValue();
2329 if ($controlValue) {
2330 $options = CRM_Core_BAO_Location::getChainSelectValues($controlValue, $controlType, TRUE);
2331 if (!$options) {
2332 $targetField->setAttribute('placeholder', $targetField->getAttribute('data-none-prompt'));
2333 }
4a44fd8a 2334 }
b71cb966 2335 else {
a3984622
OB
2336 $targetField->setAttribute('placeholder', $targetField->getAttribute('data-empty-prompt'));
2337 $targetField->setAttribute('disabled', 'disabled');
8f9c3cbe 2338 }
0db6c3e1 2339 }
a3984622 2340 // Control field not present - fall back to loading default options
0db6c3e1 2341 else {
a3984622 2342 $options = CRM_Core_PseudoConstant::$targetType();
1d07e7ab 2343 }
a3984622
OB
2344 if (!$targetField->getAttribute('multiple')) {
2345 $options = array('' => $targetField->getAttribute('placeholder')) + $options;
2346 $targetField->removeAttribute('placeholder');
2347 }
2348 $targetField->_options = array();
2349 $targetField->loadArray($options);
1d07e7ab 2350 }
1d07e7ab
CW
2351 }
2352 }
bc999cd1
CW
2353
2354 /**
2355 * Validate country / state / county match and suppress unwanted "required" errors
2356 */
2357 private function validateChainSelectFields() {
2358 foreach ($this->_chainSelectFields as $control => $target) {
a3984622 2359 if ($this->elementExists($control) && $this->elementExists($target)) {
f9f40af3 2360 $controlValue = (array) $this->getElementValue($control);
14b2ff15
CW
2361 $targetField = $this->getElement($target);
2362 $controlType = $targetField->getAttribute('data-callback') == 'civicrm/ajax/jqCounty' ? 'stateProvince' : 'country';
f9f40af3 2363 $targetValue = array_filter((array) $targetField->getValue());
14b2ff15
CW
2364 if ($targetValue || $this->getElementError($target)) {
2365 $options = CRM_Core_BAO_Location::getChainSelectValues($controlValue, $controlType, TRUE);
2366 if ($targetValue) {
2367 if (!array_intersect($targetValue, array_keys($options))) {
2368 $this->setElementError($target, $controlType == 'country' ? ts('State/Province does not match the selected Country') : ts('County does not match the selected State/Province'));
2369 }
2370 } // Suppress "required" error for field if it has no options
2371 elseif (!$options) {
2372 $this->setElementError($target, NULL);
bc999cd1
CW
2373 }
2374 }
bc999cd1
CW
2375 }
2376 }
2377 }
96025800 2378
0b50eca0 2379 /**
2380 * Assign billing name to the template.
2381 *
2382 * @param array $params
2383 * Form input params, default to $this->_params.
f3f00653 2384 *
2385 * @return string
0b50eca0 2386 */
2387 public function assignBillingName($params = array()) {
2388 $name = '';
2389 if (empty($params)) {
2390 $params = $this->_params;
2391 }
2392 if (!empty($params['billing_first_name'])) {
2393 $name = $params['billing_first_name'];
2394 }
2395
2396 if (!empty($params['billing_middle_name'])) {
2397 $name .= " {$params['billing_middle_name']}";
2398 }
2399
2400 if (!empty($params['billing_last_name'])) {
2401 $name .= " {$params['billing_last_name']}";
2402 }
2403 $name = trim($name);
2404 $this->assign('billingName', $name);
2405 return $name;
2406 }
2407
fd0770bc 2408 /**
2409 * Get the currency for the form.
2410 *
2411 * @todo this should be overriden on the forms rather than having this
2412 * historic, possible handling in here. As we clean that up we should
2413 * add deprecation notices into here.
e9bb043a 2414 *
2415 * @param array $submittedValues
2416 * Array allowed so forms inheriting this class do not break.
2417 * Ideally we would make a clear standard around how submitted values
2418 * are stored (is $this->_values consistently doing that?).
2419 *
2420 * @return string
fd0770bc 2421 */
e9bb043a 2422 public function getCurrency($submittedValues = array()) {
fd0770bc 2423 $currency = CRM_Utils_Array::value('currency', $this->_values);
2424 // For event forms, currency is in a different spot
2425 if (empty($currency)) {
2426 $currency = CRM_Utils_Array::value('currency', CRM_Utils_Array::value('event', $this->_values));
2427 }
2428 if (empty($currency)) {
2429 $currency = CRM_Utils_Request::retrieveValue('currency', 'String');
2430 }
2431 // @todo If empty there is a problem - we should probably put in a deprecation notice
2432 // to warn if that seems to be happening.
2433 return $currency;
2434 }
2435
240b0e65 2436 /**
2437 * Is the form in view or edit mode.
2438 *
2439 * The 'addField' function relies on the form action being one of a set list
2440 * of actions. Checking for these allows for an early return.
2441 *
2442 * @return bool
2443 */
2444 protected function isFormInViewOrEditMode() {
2445 return in_array($this->_action, [
2446 CRM_Core_Action::UPDATE,
2447 CRM_Core_Action::ADD,
2448 CRM_Core_Action::VIEW,
2449 CRM_Core_Action::BROWSE,
2450 CRM_Core_Action::BASIC,
2451 CRM_Core_Action::ADVANCED,
2452 CRM_Core_Action::PREVIEW,
2453 ]);
2454 }
2455
6a488035 2456}