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