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