CRM_Core_BAO_Cache - Deprecate getItems(), getItem(), setItem(), deleteGroup()
[civicrm-core.git] / CRM / Core / Form.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
fee14197 4 | CiviCRM version 5 |
6a488035 5 +--------------------------------------------------------------------+
6b83d5bd 6 | Copyright CiviCRM LLC (c) 2004-2019 |
6a488035
TO
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
9 | |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
13 | |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
18 | |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
d25dd0ee 26 */
6a488035
TO
27
28/**
29 * This is our base form. It is part of the Form/Controller/StateMachine
30 * trifecta. Each form is associated with a specific state in the state
31 * machine. Each form can also operate in various modes
32 *
33 * @package CRM
6b83d5bd 34 * @copyright CiviCRM LLC (c) 2004-2019
6a488035
TO
35 */
36
37require_once 'HTML/QuickForm/Page.php';
28518c90
EM
38
39/**
40 * Class CRM_Core_Form
41 */
6a488035
TO
42class CRM_Core_Form extends HTML_QuickForm_Page {
43
44 /**
45 * The state object that this form belongs to
46 * @var object
47 */
48 protected $_state;
49
50 /**
51 * The name of this form
52 * @var string
53 */
54 protected $_name;
55
56 /**
57 * The title of this form
58 * @var string
59 */
60 protected $_title = NULL;
61
73afe1e6 62 /**
63 * The default values for the form.
64 *
65 * @var array
66 */
67 public $_defaults = array();
68
6a488035 69 /**
2eee184e
TO
70 * (QUASI-PROTECTED) The options passed into this form
71 *
72 * This field should marked `protected` and is not generally
73 * intended for external callers, but some edge-cases do use it.
74 *
6a488035
TO
75 * @var mixed
76 */
2eee184e 77 public $_options = NULL;
6a488035
TO
78
79 /**
2eee184e
TO
80 * (QUASI-PROTECTED) The mode of operation for this form
81 *
82 * This field should marked `protected` and is not generally
83 * intended for external callers, but some edge-cases do use it.
84 *
6a488035
TO
85 * @var int
86 */
2eee184e 87 public $_action;
6a488035 88
1b9f9ca3
EM
89 /**
90 * Available payment processors.
91 *
92 * As part of trying to consolidate various payment pages we store processors here & have functions
93 * at this level to manage them.
94 *
95 * @var array
96 * An array of payment processor details with objects loaded in the 'object' field.
97 */
42e3a033 98 protected $_paymentProcessors;
1b9f9ca3
EM
99
100 /**
101 * Available payment processors (IDS).
102 *
103 * As part of trying to consolidate various payment pages we store processors here & have functions
cbcb5b49 104 * at this level to manage them. An alternative would be to have a separate Form that is inherited
105 * by all forms that allow payment processing.
1b9f9ca3
EM
106 *
107 * @var array
108 * An array of the IDS available on this form.
109 */
110 public $_paymentProcessorIDs;
111
cbcb5b49 112 /**
113 * Default or selected processor id.
114 *
115 * As part of trying to consolidate various payment pages we store processors here & have functions
116 * at this level to manage them. An alternative would be to have a separate Form that is inherited
117 * by all forms that allow payment processing.
118 *
119 * @var int
120 */
121 protected $_paymentProcessorID;
122
123 /**
124 * Is pay later enabled for the form.
125 *
126 * As part of trying to consolidate various payment pages we store processors here & have functions
127 * at this level to manage them. An alternative would be to have a separate Form that is inherited
128 * by all forms that allow payment processing.
129 *
130 * @var int
131 */
132 protected $_is_pay_later_enabled;
133
6a488035 134 /**
100fef9d 135 * The renderer used for this form
6a488035
TO
136 *
137 * @var object
138 */
139 protected $_renderer;
140
5d86176b 141 /**
142 * An array to hold a list of datefields on the form
143 * so that they can be converted to ISO in a consistent manner
144 *
145 * @var array
146 *
147 * e.g on a form declare $_dateFields = array(
148 * 'receive_date' => array('default' => 'now'),
149 * );
150 * then in postProcess call $this->convertDateFieldsToMySQL($formValues)
151 * to have the time field re-incorporated into the field & 'now' set if
152 * no value has been passed in
153 */
154 protected $_dateFields = array();
155
6a488035 156 /**
100fef9d 157 * Cache the smarty template for efficiency reasons
6a488035
TO
158 *
159 * @var CRM_Core_Smarty
160 */
161 static protected $_template;
162
461fa5fb 163 /**
164 * Indicate if this form should warn users of unsaved changes
165 */
166 protected $unsavedChangesWarn;
167
03a7ec8f 168 /**
fc05b8da 169 * What to return to the client if in ajax mode (snippet=json)
03a7ec8f
CW
170 *
171 * @var array
172 */
173 public $ajaxResponse = array();
174
118e964e
CW
175 /**
176 * Url path used to reach this page
177 *
178 * @var array
179 */
180 public $urlPath = array();
181
2d69ef96 182 /**
183 * Context of the form being loaded.
184 *
185 * 'event' or null
186 *
187 * @var string
188 */
189 protected $context;
190
191 /**
192 * @return string
193 */
194 public function getContext() {
195 return $this->context;
196 }
197
198 /**
199 * Set context variable.
200 */
201 public function setContext() {
202 $this->context = CRM_Utils_Request::retrieve('context', 'Alphanumeric', $this);
203 }
204
d77a0a58
EM
205 /**
206 * @var CRM_Core_Controller
207 */
208 public $controller;
4a44fd8a 209
6a488035 210 /**
100fef9d 211 * Constants for attributes for various form elements
6a488035
TO
212 * attempt to standardize on the number of variations that we
213 * use of the below form elements
214 *
215 * @var const string
216 */
7da04cde 217 const ATTR_SPACING = '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;';
6a488035
TO
218
219 /**
220 * All checkboxes are defined with a common prefix. This allows us to
221 * have the same javascript to check / clear all the checkboxes etc
222 * If u have multiple groups of checkboxes, you will need to give them different
223 * ids to avoid potential name collision
224 *
fffe9ee1 225 * @var string|int
6a488035 226 */
7da04cde 227 const CB_PREFIX = 'mark_x_', CB_PREFIY = 'mark_y_', CB_PREFIZ = 'mark_z_', CB_PREFIX_LEN = 7;
6a488035 228
1d07e7ab
CW
229 /**
230 * @internal to keep track of chain-select fields
231 * @var array
232 */
233 private $_chainSelectFields = array();
234
d8f1758d
CW
235 /**
236 * Extra input types we support via the "add" method
237 * @var array
238 */
239 public static $html5Types = array(
240 'number',
241 'url',
242 'email',
1192bd09 243 'color',
d8f1758d
CW
244 );
245
6a488035 246 /**
4b62bc4f 247 * Constructor for the basic form page.
6a488035
TO
248 *
249 * We should not use QuickForm directly. This class provides a lot
250 * of default convenient functions, rules and buttons
251 *
6a0b768e
TO
252 * @param object $state
253 * State associated with this form.
fffe9ee1 254 * @param \const|\enum|int $action The mode the form is operating in (None/Create/View/Update/Delete)
6a0b768e
TO
255 * @param string $method
256 * The type of http method used (GET/POST).
257 * @param string $name
258 * The name of the form if different from class name.
6a488035 259 *
dd244018 260 * @return \CRM_Core_Form
6a488035 261 */
2da40d21 262 public function __construct(
6a488035
TO
263 $state = NULL,
264 $action = CRM_Core_Action::NONE,
265 $method = 'post',
f9f40af3 266 $name = NULL
6a488035
TO
267 ) {
268
269 if ($name) {
270 $this->_name = $name;
271 }
272 else {
b50fdacc 273 // CRM-15153 - FIXME this name translates to a DOM id and is not always unique!
6a488035
TO
274 $this->_name = CRM_Utils_String::getClassName(CRM_Utils_System::getClassName($this));
275 }
276
6ef04c72 277 parent::__construct($this->_name, $method);
6a488035
TO
278
279 $this->_state =& $state;
280 if ($this->_state) {
281 $this->_state->setName($this->_name);
282 }
283 $this->_action = (int) $action;
284
285 $this->registerRules();
286
287 // let the constructor initialize this, should happen only once
288 if (!isset(self::$_template)) {
289 self::$_template = CRM_Core_Smarty::singleton();
290 }
b50fdacc
CW
291 // Workaround for CRM-15153 - give each form a reasonably unique css class
292 $this->addClass(CRM_Utils_System::getClassName($this));
03a7ec8f 293
819d4cbb 294 $this->assign('snippet', CRM_Utils_Array::value('snippet', $_GET));
d84ae5f6 295 $this->setTranslatedFields();
6a488035
TO
296 }
297
d84ae5f6 298 /**
299 * Set translated fields.
300 *
301 * This function is called from the class constructor, allowing us to set
302 * fields on the class that can't be set as properties due to need for
303 * translation or other non-input specific handling.
304 */
305 protected function setTranslatedFields() {}
306
023e90c3 307 /**
e51d62d3 308 * Add one or more css classes to the form.
309 *
100fef9d 310 * @param string $className
023e90c3
CW
311 */
312 public function addClass($className) {
313 $classes = $this->getAttribute('class');
314 $this->setAttribute('class', ($classes ? "$classes " : '') . $className);
315 }
316
6a488035 317 /**
fe482240 318 * Register all the standard rules that most forms potentially use.
6a488035 319 */
00be9182 320 public function registerRules() {
6a488035 321 static $rules = array(
353ffa53
TO
322 'title',
323 'longTitle',
324 'variable',
325 'qfVariable',
326 'phone',
327 'integer',
328 'query',
329 'url',
330 'wikiURL',
331 'domain',
332 'numberOfDigit',
333 'date',
334 'currentDate',
335 'asciiFile',
336 'htmlFile',
337 'utf8File',
338 'objectExists',
339 'optionExists',
340 'postalCode',
341 'money',
342 'positiveInteger',
343 'xssString',
344 'fileExists',
d9d7e7dd 345 'settingPath',
353ffa53
TO
346 'autocomplete',
347 'validContact',
6a488035
TO
348 );
349
350 foreach ($rules as $rule) {
351 $this->registerRule($rule, 'callback', $rule, 'CRM_Utils_Rule');
352 }
353 }
354
355 /**
e51d62d3 356 * Simple easy to use wrapper around addElement.
357 *
358 * Deal with simple validation rules.
6a488035 359 *
bae056c6
CW
360 * @param string $type
361 * @param string $name
77b97be7 362 * @param string $label
bae056c6 363 * @param string|array $attributes (options for select elements)
77b97be7 364 * @param bool $required
6a0b768e
TO
365 * @param array $extra
366 * (attributes for select elements).
d86e674f 367 * For datepicker elements this is consistent with the data
368 * from CRM_Utils_Date::getDatePickerExtra
6a488035 369 *
e51d62d3 370 * @return HTML_QuickForm_Element
371 * Could be an error object
6a488035 372 */
2da40d21 373 public function &add(
f9f40af3 374 $type, $name, $label = '',
908fe4e6 375 $attributes = '', $required = FALSE, $extra = NULL
6a488035 376 ) {
092cb9c5 377 // Fudge some extra types that quickform doesn't support
1192bd09 378 $inputType = $type;
d8f1758d 379 if ($type == 'wysiwyg' || in_array($type, self::$html5Types)) {
66659cd7 380 $attributes = ($attributes ? $attributes : array()) + array('class' => '');
092cb9c5 381 $attributes['class'] = ltrim($attributes['class'] . " crm-form-$type");
7ad5ae6a
CW
382 if ($type == 'wysiwyg' && isset($attributes['preset'])) {
383 $attributes['data-preset'] = $attributes['preset'];
384 unset($attributes['preset']);
385 }
092cb9c5 386 $type = $type == 'wysiwyg' ? 'textarea' : 'text';
b608cfb1 387 }
b733747a
CW
388 // Like select but accepts rich array data (with nesting, colors, icons, etc) as option list.
389 if ($inputType == 'select2') {
390 $type = 'text';
391 $options = $attributes;
392 $attributes = $attributes = ($extra ? $extra : array()) + array('class' => '');
393 $attributes['class'] = ltrim($attributes['class'] . " crm-select2 crm-form-select2");
394 $attributes['data-select-params'] = json_encode(array('data' => $options, 'multiple' => !empty($attributes['multiple'])));
395 unset($attributes['multiple']);
396 $extra = NULL;
397 }
a2973721 398 // @see http://wiki.civicrm.org/confluence/display/CRMDOC/crmDatepicker
238fee7f 399 if ($type == 'datepicker') {
a2973721 400 $attributes = ($attributes ? $attributes : array());
238fee7f 401 $attributes['data-crm-datepicker'] = json_encode((array) $extra);
1f1410dc 402 if (!empty($attributes['aria-label']) || $label) {
403 $attributes['aria-label'] = CRM_Utils_Array::value('aria-label', $attributes, $label);
404 }
238fee7f
CW
405 $type = "text";
406 }
1d07e7ab 407 if ($type == 'select' && is_array($extra)) {
bae056c6 408 // Normalize this property
1d07e7ab
CW
409 if (!empty($extra['multiple'])) {
410 $extra['multiple'] = 'multiple';
411 }
412 else {
413 unset($extra['multiple']);
414 }
65e8615b 415 unset($extra['size'], $extra['maxlength']);
bae056c6
CW
416 // Add placeholder option for select
417 if (isset($extra['placeholder'])) {
418 if ($extra['placeholder'] === TRUE) {
419 $extra['placeholder'] = $required ? ts('- select -') : ts('- none -');
420 }
421 if (($extra['placeholder'] || $extra['placeholder'] === '') && empty($extra['multiple']) && is_array($attributes) && !isset($attributes[''])) {
422 $attributes = array('' => $extra['placeholder']) + $attributes;
423 }
424 }
908fe4e6
CW
425 }
426 $element = $this->addElement($type, $name, $label, $attributes, $extra);
6a488035
TO
427 if (HTML_QuickForm::isError($element)) {
428 CRM_Core_Error::fatal(HTML_QuickForm::errorMessage($element));
429 }
430
1192bd09
CW
431 if ($inputType == 'color') {
432 $this->addRule($name, ts('%1 must contain a color value e.g. #ffffff.', array(1 => $label)), 'regex', '/#[0-9a-fA-F]{6}/');
433 }
434
6a488035
TO
435 if ($required) {
436 if ($type == 'file') {
437 $error = $this->addRule($name, ts('%1 is a required field.', array(1 => $label)), 'uploadedfile');
438 }
439 else {
440 $error = $this->addRule($name, ts('%1 is a required field.', array(1 => $label)), 'required');
441 }
442 if (HTML_QuickForm::isError($error)) {
443 CRM_Core_Error::fatal(HTML_QuickForm::errorMessage($element));
444 }
445 }
446
b3ee84c9
MD
447 // Add context for the editing of option groups
448 if (isset($extra['option_context'])) {
449 $context = json_encode($extra['option_context']);
450 $element->setAttribute('data-option-edit-context', $context);
451 }
452
6a488035
TO
453 return $element;
454 }
455
456 /**
e51d62d3 457 * Preprocess form.
458 *
459 * This is called before buildForm. Any pre-processing that
460 * needs to be done for buildForm should be done here.
6a488035 461 *
8eedd10a 462 * This is a virtual function and should be redefined if needed.
6a488035 463 */
f9f40af3
TO
464 public function preProcess() {
465 }
6a488035
TO
466
467 /**
8eedd10a 468 * Called after the form is validated.
469 *
470 * Any processing of form state etc should be done in this function.
6a488035
TO
471 * Typically all processing associated with a form should be done
472 * here and relevant state should be stored in the session
473 *
474 * This is a virtual function and should be redefined if needed
6a488035 475 */
f9f40af3
TO
476 public function postProcess() {
477 }
6a488035
TO
478
479 /**
e51d62d3 480 * Main process wrapper.
481 *
482 * Implemented so that we can call all the hook functions.
483 *
6a0b768e
TO
484 * @param bool $allowAjax
485 * FIXME: This feels kind of hackish, ideally we would take the json-related code from this function.
7e9fdecf 486 * and bury it deeper down in the controller
6a488035 487 */
00be9182 488 public function mainProcess($allowAjax = TRUE) {
6a488035 489 $this->postProcess();
6a488035 490 $this->postProcessHook();
03a7ec8f 491
fc05b8da 492 // Respond with JSON if in AJAX context (also support legacy value '6')
353ffa53
TO
493 if ($allowAjax && !empty($_REQUEST['snippet']) && in_array($_REQUEST['snippet'], array(
494 CRM_Core_Smarty::PRINT_JSON,
af9b09df 495 6,
353ffa53
TO
496 ))
497 ) {
03a7ec8f
CW
498 $this->ajaxResponse['buttonName'] = str_replace('_qf_' . $this->getAttribute('id') . '_', '', $this->controller->getButtonName());
499 $this->ajaxResponse['action'] = $this->_action;
18ddc127
CW
500 if (isset($this->_id) || isset($this->id)) {
501 $this->ajaxResponse['id'] = isset($this->id) ? $this->id : $this->_id;
502 }
03a7ec8f
CW
503 CRM_Core_Page_AJAX::returnJsonResponse($this->ajaxResponse);
504 }
6a488035
TO
505 }
506
507 /**
4b62bc4f 508 * The postProcess hook is typically called by the framework.
e51d62d3 509 *
6a488035
TO
510 * However in a few cases, the form exits or redirects early in which
511 * case it needs to call this function so other modules can do the needful
512 * Calling this function directly should be avoided if possible. In general a
513 * better way is to do setUserContext so the framework does the redirect
6a488035 514 */
00be9182 515 public function postProcessHook() {
6a488035
TO
516 CRM_Utils_Hook::postProcess(get_class($this), $this);
517 }
518
519 /**
8eedd10a 520 * This virtual function is used to build the form.
6a488035 521 *
8eedd10a 522 * It replaces the buildForm associated with QuickForm_Page. This allows us to put
523 * preProcess in front of the actual form building routine
6a488035 524 */
f9f40af3
TO
525 public function buildQuickForm() {
526 }
6a488035
TO
527
528 /**
8eedd10a 529 * This virtual function is used to set the default values of various form elements.
6a488035 530 *
a1a2a83d 531 * @return array|NULL
a6c01b45 532 * reference to the array of default values
6a488035 533 */
f9f40af3 534 public function setDefaultValues() {
a1a2a83d 535 return NULL;
f9f40af3 536 }
6a488035
TO
537
538 /**
8eedd10a 539 * This is a virtual function that adds group and global rules to the form.
6a488035 540 *
8eedd10a 541 * Keeping it distinct from the form to keep code small
542 * and localized in the form building code
6a488035 543 */
f9f40af3
TO
544 public function addRules() {
545 }
6a488035 546
b5c2afd0 547 /**
fe482240 548 * Performs the server side validation.
b5c2afd0 549 * @since 1.0
5c766a0b 550 * @return bool
a6c01b45 551 * true if no error found
b5c2afd0
EM
552 * @throws HTML_QuickForm_Error
553 */
00be9182 554 public function validate() {
6a488035
TO
555 $error = parent::validate();
556
bc999cd1
CW
557 $this->validateChainSelectFields();
558
6a9e89b4 559 $hookErrors = array();
6a488035
TO
560
561 CRM_Utils_Hook::validateForm(
562 get_class($this),
563 $this->_submitValues,
564 $this->_submitFiles,
565 $this,
566 $hookErrors
567 );
568
569 if (!empty($hookErrors)) {
570 $this->_errors += $hookErrors;
571 }
572
573 return (0 == count($this->_errors));
574 }
575
576 /**
3bdf1f3a 577 * Core function that builds the form.
578 *
579 * We redefine this function here and expect all CRM forms to build their form in the function
6a488035 580 * buildQuickForm.
6a488035 581 */
00be9182 582 public function buildForm() {
6a488035
TO
583 $this->_formBuilt = TRUE;
584
585 $this->preProcess();
586
21d2903d
AN
587 CRM_Utils_Hook::preProcess(get_class($this), $this);
588
6a488035
TO
589 $this->assign('translatePermission', CRM_Core_Permission::check('translate CiviCRM'));
590
591 if (
592 $this->controller->_key &&
593 $this->controller->_generateQFKey
594 ) {
595 $this->addElement('hidden', 'qfKey', $this->controller->_key);
596 $this->assign('qfKey', $this->controller->_key);
ab435bd4 597
6a488035
TO
598 }
599
ab435bd4
DL
600 // _generateQFKey suppresses the qfKey generation on form snippets that
601 // are part of other forms, hence we use that to avoid adding entryURL
602 if ($this->controller->_generateQFKey && $this->controller->_entryURL) {
3ab88a8c
DL
603 $this->addElement('hidden', 'entryURL', $this->controller->_entryURL);
604 }
6a488035
TO
605
606 $this->buildQuickForm();
607
608 $defaults = $this->setDefaultValues();
609 unset($defaults['qfKey']);
610
611 if (!empty($defaults)) {
612 $this->setDefaults($defaults);
613 }
614
615 // call the form hook
b44e3f84 616 // also call the hook function so any modules can set their own custom defaults
6a488035
TO
617 // the user can do both the form and set default values with this hook
618 CRM_Utils_Hook::buildForm(get_class($this), $this);
619
620 $this->addRules();
3e201321 621
622 //Set html data-attribute to enable warning user of unsaved changes
f9f40af3 623 if ($this->unsavedChangesWarn === TRUE
353ffa53
TO
624 || (!isset($this->unsavedChangesWarn)
625 && ($this->_action & CRM_Core_Action::ADD || $this->_action & CRM_Core_Action::UPDATE)
626 )
627 ) {
f9f40af3 628 $this->setAttribute('data-warn-changes', 'true');
3e201321 629 }
6a488035
TO
630 }
631
632 /**
3bdf1f3a 633 * Add default Next / Back buttons.
6a488035 634 *
6c552737
TO
635 * @param array $params
636 * Array of associative arrays in the order in which the buttons should be
637 * displayed. The associate array has 3 fields: 'type', 'name' and 'isDefault'
638 * The base form class will define a bunch of static arrays for commonly used
639 * formats.
6a488035 640 */
00be9182 641 public function addButtons($params) {
deae896d 642 $prevnext = $spacing = array();
6a488035 643 foreach ($params as $button) {
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
e2123607 1289 /**
1290 * Add a search for a range using date picker fields.
1291 *
1292 * @param string $fieldName
1293 * @param string $label
1294 * @param bool $required
1295 * @param string $fromLabel
1296 * @param string $toLabel
1297 */
1298 public function addDatePickerRange($fieldName, $label, $required = FALSE, $fromLabel = 'From', $toLabel = 'To') {
1299
1300 $options = array(
1301 '' => ts('- any -'),
1302 0 => ts('Choose Date Range'),
1303 ) + CRM_Core_OptionGroup::values('relative_date_filters');
1304
1305 $this->add('select',
1306 "{$fieldName}_relative",
1307 $label,
1308 $options,
1309 $required,
1310 NULL
1311 );
1312 $attributes = ['format' => 'searchDate'];
1313 $extra = ['time' => FALSE];
1314 $this->add('datepicker', $fieldName . '_low', ts($fromLabel), $attributes, $required, $extra);
1315 $this->add('datepicker', $fieldName . '_high', ts($toLabel), $attributes, $required, $extra);
1316 }
1317
03225ad6
CW
1318 /**
1319 * Based on form action, return a string representing the api action.
1320 * Used by addField method.
1321 *
1322 * Return string
1323 */
d5e4784e 1324 protected function getApiAction() {
03225ad6
CW
1325 $action = $this->getAction();
1326 if ($action & (CRM_Core_Action::UPDATE + CRM_Core_Action::ADD)) {
1327 return 'create';
1328 }
889dbed8 1329 if ($action & (CRM_Core_Action::VIEW + CRM_Core_Action::BROWSE + CRM_Core_Action::BASIC + CRM_Core_Action::ADVANCED + CRM_Core_Action::PREVIEW)) {
03225ad6
CW
1330 return 'get';
1331 }
1332 // If you get this exception try adding more cases above.
0e02cb01 1333 throw new Exception("Cannot determine api action for " . get_class($this) . '.' . 'CRM_Core_Action "' . CRM_Core_Action::description($action) . '" not recognized.');
03225ad6
CW
1334 }
1335
6e62b28c 1336 /**
d5965a37 1337 * Classes extending CRM_Core_Form should implement this method.
6e62b28c
TM
1338 * @throws Exception
1339 */
1340 public function getDefaultEntity() {
0e02cb01 1341 throw new Exception("Cannot determine default entity. " . get_class($this) . " should implement getDefaultEntity().");
6e62b28c 1342 }
6a488035 1343
1ae720b3
TM
1344 /**
1345 * Classes extending CRM_Core_Form should implement this method.
1346 *
1347 * TODO: Merge with CRM_Core_DAO::buildOptionsContext($context) and add validation.
1348 * @throws Exception
1349 */
1350 public function getDefaultContext() {
0e02cb01 1351 throw new Exception("Cannot determine default context. " . get_class($this) . " should implement getDefaultContext().");
1ae720b3
TM
1352 }
1353
5fafc9b0 1354 /**
fe482240 1355 * Adds a select based on field metadata.
5fafc9b0 1356 * TODO: This could be even more generic and widget type (select in this case) could also be read from metadata
475e9f44 1357 * Perhaps a method like $form->bind($name) which would look up all metadata for named field
6a0b768e
TO
1358 * @param $name
1359 * Field name to go on the form.
1360 * @param array $props
1361 * Mix of html attributes and special properties, namely.
920600e1
CW
1362 * - entity (api entity name, can usually be inferred automatically from the form class)
1363 * - field (field name - only needed if different from name used on the form)
1364 * - option_url - path to edit this option list - usually retrieved automatically - set to NULL to disable link
1365 * - placeholder - set to NULL to disable
d0def949 1366 * - multiple - bool
76773c5a 1367 * - context - @see CRM_Core_DAO::buildOptionsContext
5fafc9b0
CW
1368 * @param bool $required
1369 * @throws CRM_Core_Exception
1370 * @return HTML_QuickForm_Element
1371 */
00be9182 1372 public function addSelect($name, $props = array(), $required = FALSE) {
920600e1 1373 if (!isset($props['entity'])) {
6e62b28c 1374 $props['entity'] = $this->getDefaultEntity();
6a488035 1375 }
920600e1
CW
1376 if (!isset($props['field'])) {
1377 $props['field'] = strrpos($name, '[') ? rtrim(substr($name, 1 + strrpos($name, '[')), ']') : $name;
e869b07d 1378 }
65e8615b
CW
1379 if (!isset($props['context'])) {
1380 try {
1381 $props['context'] = $this->getDefaultContext();
1382 }
1383 // This is not a required param, so we'll ignore if this doesn't exist.
1384 catch (Exception $e) {}
1385 }
f76b27fe
CW
1386 // Fetch options from the api unless passed explicitly
1387 if (isset($props['options'])) {
1388 $options = $props['options'];
1389 }
1390 else {
76352fbc 1391 $info = civicrm_api3($props['entity'], 'getoptions', $props);
f76b27fe
CW
1392 $options = $info['values'];
1393 }
5fafc9b0 1394 if (!array_key_exists('placeholder', $props)) {
76773c5a 1395 $props['placeholder'] = $required ? ts('- select -') : CRM_Utils_Array::value('context', $props) == 'search' ? ts('- any -') : ts('- none -');
5fafc9b0 1396 }
5fafc9b0
CW
1397 // Handle custom field
1398 if (strpos($name, 'custom_') === 0 && is_numeric($name[7])) {
1399 list(, $id) = explode('_', $name);
1400 $label = isset($props['label']) ? $props['label'] : CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomField', 'label', $id);
475e9f44 1401 $gid = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomField', 'option_group_id', $id);
76773c5a
CW
1402 if (CRM_Utils_Array::value('context', $props) != 'search') {
1403 $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);
1404 }
5fafc9b0
CW
1405 }
1406 // Core field
6a488035 1407 else {
f76b27fe 1408 $info = civicrm_api3($props['entity'], 'getfields');
22e263ad 1409 foreach ($info['values'] as $uniqueName => $fieldSpec) {
e869b07d 1410 if (
920600e1
CW
1411 $uniqueName === $props['field'] ||
1412 CRM_Utils_Array::value('name', $fieldSpec) === $props['field'] ||
1413 in_array($props['field'], CRM_Utils_Array::value('api.aliases', $fieldSpec, array()))
e869b07d
CW
1414 ) {
1415 break;
1416 }
6a488035 1417 }
e869b07d 1418 $label = isset($props['label']) ? $props['label'] : $fieldSpec['title'];
76773c5a 1419 if (CRM_Utils_Array::value('context', $props) != 'search') {
599ae208 1420 $props['data-option-edit-path'] = array_key_exists('option_url', $props) ? $props['option_url'] : CRM_Core_PseudoConstant::getOptionEditUrl($fieldSpec);
76773c5a 1421 }
6a488035 1422 }
920600e1
CW
1423 $props['class'] = (isset($props['class']) ? $props['class'] . ' ' : '') . "crm-select2";
1424 $props['data-api-entity'] = $props['entity'];
1425 $props['data-api-field'] = $props['field'];
76773c5a 1426 CRM_Utils_Array::remove($props, 'label', 'entity', 'field', 'option_url', 'options', 'context');
5fafc9b0 1427 return $this->add('select', $name, $label, $options, $required, $props);
6a488035
TO
1428 }
1429
7ec4548b
TM
1430 /**
1431 * Adds a field based on metadata.
1432 *
1433 * @param $name
1434 * Field name to go on the form.
1435 * @param array $props
1436 * Mix of html attributes and special properties, namely.
1437 * - entity (api entity name, can usually be inferred automatically from the form class)
03225ad6 1438 * - name (field name - only needed if different from name used on the form)
7ec4548b
TM
1439 * - option_url - path to edit this option list - usually retrieved automatically - set to NULL to disable link
1440 * - placeholder - set to NULL to disable
1441 * - multiple - bool
1442 * - context - @see CRM_Core_DAO::buildOptionsContext
1443 * @param bool $required
ed0ca248 1444 * @param bool $legacyDate
1445 * Temporary param to facilitate the conversion of fields to use the datepicker in
1446 * a controlled way. To convert the field the jcalendar code needs to be removed from the
1447 * tpl as well. That file is intended to be EOL.
1448 *
03225ad6
CW
1449 * @throws \CiviCRM_API3_Exception
1450 * @throws \Exception
2a300b65 1451 * @return HTML_QuickForm_Element
7ec4548b 1452 */
ed0ca248 1453 public function addField($name, $props = array(), $required = FALSE, $legacyDate = TRUE) {
1ae720b3 1454 // Resolve context.
916b6181 1455 if (empty($props['context'])) {
1ae720b3
TM
1456 $props['context'] = $this->getDefaultContext();
1457 }
916b6181 1458 $context = $props['context'];
7ec4548b 1459 // Resolve entity.
916b6181 1460 if (empty($props['entity'])) {
7ec4548b
TM
1461 $props['entity'] = $this->getDefaultEntity();
1462 }
1463 // Resolve field.
916b6181 1464 if (empty($props['name'])) {
03225ad6 1465 $props['name'] = strrpos($name, '[') ? rtrim(substr($name, 1 + strrpos($name, '[')), ']') : $name;
7ec4548b 1466 }
03225ad6 1467 // Resolve action.
916b6181 1468 if (empty($props['action'])) {
03225ad6 1469 $props['action'] = $this->getApiAction();
7ec4548b 1470 }
2b31bc15
CW
1471
1472 // Handle custom fields
1473 if (strpos($name, 'custom_') === 0 && is_numeric($name[7])) {
1474 $fieldId = (int) substr($name, 7);
916b6181 1475 return CRM_Core_BAO_CustomField::addQuickFormElement($this, $name, $fieldId, $required, $context == 'search', CRM_Utils_Array::value('label', $props));
2b31bc15
CW
1476 }
1477
1478 // Core field - get metadata.
d60a6fba 1479 $fieldSpec = civicrm_api3($props['entity'], 'getfield', $props);
03225ad6 1480 $fieldSpec = $fieldSpec['values'];
80a96508 1481 $fieldSpecLabel = isset($fieldSpec['html']['label']) ? $fieldSpec['html']['label'] : CRM_Utils_Array::value('title', $fieldSpec);
1482 $label = CRM_Utils_Array::value('label', $props, $fieldSpecLabel);
7ec4548b 1483
7ec4548b 1484 $widget = isset($props['type']) ? $props['type'] : $fieldSpec['html']['type'];
916b6181 1485 if ($widget == 'TextArea' && $context == 'search') {
7ec4548b
TM
1486 $widget = 'Text';
1487 }
1488
1489 $isSelect = (in_array($widget, array(
1490 'Select',
dd4706ef
TM
1491 'CheckBoxGroup',
1492 'RadioGroup',
7ec4548b
TM
1493 'Radio',
1494 )));
1495
1496 if ($isSelect) {
2f32ed10 1497 // Fetch options from the api unless passed explicitly.
7ec4548b
TM
1498 if (isset($props['options'])) {
1499 $options = $props['options'];
1500 }
1501 else {
a4969aee 1502 $options = isset($fieldSpec['options']) ? $fieldSpec['options'] : NULL;
7ec4548b 1503 }
916b6181 1504 if ($context == 'search') {
7ec4548b 1505 $widget = 'Select';
65e8615b 1506 $props['multiple'] = CRM_Utils_Array::value('multiple', $props, TRUE);
7ec4548b 1507 }
7ec4548b
TM
1508
1509 // Add data for popup link.
3ef93345
MD
1510 $canEditOptions = CRM_Core_Permission::check('administer CiviCRM');
1511 $hasOptionUrl = !empty($props['option_url']);
1512 $optionUrlKeyIsSet = array_key_exists('option_url', $props);
1513 $shouldAdd = $context !== 'search' && $isSelect && $canEditOptions;
1514
1515 // Only add if key is not set, or if non-empty option url is provided
1516 if (($hasOptionUrl || !$optionUrlKeyIsSet) && $shouldAdd) {
1517 $optionUrl = $hasOptionUrl ? $props['option_url'] :
1518 CRM_Core_PseudoConstant::getOptionEditUrl($fieldSpec);
1519 $props['data-option-edit-path'] = $optionUrl;
7ec4548b 1520 $props['data-api-entity'] = $props['entity'];
03225ad6 1521 $props['data-api-field'] = $props['name'];
7ec4548b
TM
1522 }
1523 }
95457d69 1524 $props += CRM_Utils_Array::value('html', $fieldSpec, array());
65e8615b 1525 CRM_Utils_Array::remove($props, 'entity', 'name', 'context', 'label', 'action', 'type', 'option_url', 'options');
599ae208 1526
b44e3f84 1527 // TODO: refactor switch statement, to separate methods.
7ec4548b
TM
1528 switch ($widget) {
1529 case 'Text':
d8f1758d
CW
1530 case 'Url':
1531 case 'Number':
1532 case 'Email':
7ec4548b 1533 //TODO: Autodetect ranges
5b8080ad 1534 $props['size'] = isset($props['size']) ? $props['size'] : 60;
d8f1758d 1535 return $this->add(strtolower($widget), $name, $label, $props, $required);
7ec4548b 1536
b4b53245 1537 case 'hidden':
2a300b65 1538 return $this->add('hidden', $name, NULL, $props, $required);
b4b53245 1539
0efbca68
TM
1540 case 'TextArea':
1541 //Set default columns and rows for textarea.
1542 $props['rows'] = isset($props['rows']) ? $props['rows'] : 4;
1543 $props['cols'] = isset($props['cols']) ? $props['cols'] : 60;
079f52de 1544 if (empty($props['maxlength']) && isset($fieldSpec['length'])) {
ed71bbca 1545 $props['maxlength'] = $fieldSpec['length'];
1546 }
599ae208 1547 return $this->add('textarea', $name, $label, $props, $required);
0efbca68 1548
db3ec100 1549 case 'Select Date':
ed0ca248 1550 // This is a white list for fields that have been tested with
1551 // date picker. We should be able to remove the other
1552 if ($legacyDate) {
1553 //TODO: add range support
1554 //TODO: Add date formats
1555 //TODO: Add javascript template for dates.
1556 return $this->addDate($name, $label, $required, $props);
1557 }
1558 else {
1559 $fieldSpec = CRM_Utils_Date::addDateMetadataToField($fieldSpec, $fieldSpec);
1560 $attributes = array('format' => $fieldSpec['date_format']);
1561 return $this->add('datepicker', $name, $label, $attributes, $required, $fieldSpec['datepicker']['extra']);
1562 }
db3ec100 1563
a4969aee
TM
1564 case 'Radio':
1565 $separator = isset($props['separator']) ? $props['separator'] : NULL;
125d54e1 1566 unset($props['separator']);
ef3a048a 1567 if (!isset($props['allowClear'])) {
125d54e1 1568 $props['allowClear'] = !$required;
ef3a048a 1569 }
2a300b65 1570 return $this->addRadio($name, $label, $options, $props, $separator, $required);
a4969aee 1571
b248d52b
CW
1572 case 'ChainSelect':
1573 $props += array(
1574 'required' => $required,
1575 'label' => $label,
916b6181 1576 'multiple' => $context == 'search',
b248d52b
CW
1577 );
1578 return $this->addChainSelect($name, $props);
1579
7ec4548b 1580 case 'Select':
b248d52b 1581 $props['class'] = CRM_Utils_Array::value('class', $props, 'big') . ' crm-select2';
65e8615b 1582 if (!array_key_exists('placeholder', $props)) {
78e1efac 1583 $props['placeholder'] = $required ? ts('- select -') : ($context == 'search' ? ts('- any -') : ts('- none -'));
7ec4548b 1584 }
7ec4548b 1585 // TODO: Add and/or option for fields that store multiple values
b2da03d6 1586 return $this->add('select', $name, $label, $options, $required, $props);
7ec4548b 1587
dd4706ef 1588 case 'CheckBoxGroup':
2a300b65 1589 return $this->addCheckBox($name, $label, array_flip($options), $required, $props);
dd4706ef
TM
1590
1591 case 'RadioGroup':
2a300b65 1592 return $this->addRadio($name, $label, $options, $props, NULL, $required);
dd4706ef 1593
a4969aee 1594 case 'CheckBox':
999ab5e1
TM
1595 $text = isset($props['text']) ? $props['text'] : NULL;
1596 unset($props['text']);
2a300b65 1597 return $this->addElement('checkbox', $name, $label, $text, $props);
a4969aee 1598
50471995 1599 //add support for 'Advcheckbox' field
1600 case 'advcheckbox':
b0964781 1601 $text = isset($props['text']) ? $props['text'] : NULL;
1602 unset($props['text']);
1603 return $this->addElement('advcheckbox', $name, $label, $text, $props);
50471995 1604
33fa033c
TM
1605 case 'File':
1606 // We should not build upload file in search mode.
916b6181 1607 if ($context == 'search') {
33fa033c
TM
1608 return;
1609 }
2a300b65 1610 $file = $this->add('file', $name, $label, $props, $required);
33fa033c 1611 $this->addUploadElement($name);
2a300b65 1612 return $file;
33fa033c 1613
b66c1d2c
CW
1614 case 'RichTextEditor':
1615 return $this->add('wysiwyg', $name, $label, $props, $required);
1616
b58770ea 1617 case 'EntityRef':
2a300b65 1618 return $this->addEntityRef($name, $label, $props, $required);
b58770ea 1619
e9bc5dcc 1620 case 'Password':
a7e59a48 1621 $props['size'] = isset($props['size']) ? $props['size'] : 60;
e9bc5dcc
SL
1622 return $this->add('password', $name, $label, $props, $required);
1623
7ec4548b
TM
1624 // Check datatypes of fields
1625 // case 'Int':
1626 //case 'Float':
1627 //case 'Money':
7ec4548b
TM
1628 //case read only fields
1629 default:
1630 throw new Exception("Unsupported html-element " . $widget);
1631 }
1632 }
1633
6a488035
TO
1634 /**
1635 * Add a widget for selecting/editing/creating/copying a profile form
1636 *
6a0b768e
TO
1637 * @param string $name
1638 * HTML form-element name.
1639 * @param string $label
1640 * Printable label.
1641 * @param string $allowCoreTypes
1642 * Only present a UFGroup if its group_type includes a subset of $allowCoreTypes; e.g. 'Individual', 'Activity'.
1643 * @param string $allowSubTypes
1644 * Only present a UFGroup if its group_type is compatible with $allowSubypes.
6a488035 1645 * @param array $entities
6a0b768e
TO
1646 * @param bool $default
1647 * //CRM-15427.
54957108 1648 * @param string $usedFor
6a488035 1649 */
37375016 1650 public function addProfileSelector($name, $label, $allowCoreTypes, $allowSubTypes, $entities, $default = FALSE, $usedFor = NULL) {
6a488035
TO
1651 // Output widget
1652 // FIXME: Instead of adhoc serialization, use a single json_encode()
1653 CRM_UF_Page_ProfileEditor::registerProfileScripts();
1654 CRM_UF_Page_ProfileEditor::registerSchemas(CRM_Utils_Array::collect('entity_type', $entities));
1655 $this->add('text', $name, $label, array(
1656 'class' => 'crm-profile-selector',
1657 // Note: client treats ';;' as equivalent to \0, and ';;' works better in HTML
1658 'data-group-type' => CRM_Core_BAO_UFGroup::encodeGroupType($allowCoreTypes, $allowSubTypes, ';;'),
1659 'data-entities' => json_encode($entities),
99e239bc 1660 //CRM-15427
1661 'data-default' => $default,
37375016 1662 'data-usedfor' => json_encode($usedFor),
6a488035
TO
1663 ));
1664 }
1665
a0ee3941
EM
1666 /**
1667 * @return null
1668 */
6a488035
TO
1669 public function getRootTitle() {
1670 return NULL;
1671 }
1672
a0ee3941
EM
1673 /**
1674 * @return string
1675 */
6a488035
TO
1676 public function getCompleteTitle() {
1677 return $this->getRootTitle() . $this->getTitle();
1678 }
1679
a0ee3941
EM
1680 /**
1681 * @return CRM_Core_Smarty
1682 */
00be9182 1683 public static function &getTemplate() {
6a488035
TO
1684 return self::$_template;
1685 }
1686
a0ee3941
EM
1687 /**
1688 * @param $elementName
1689 */
00be9182 1690 public function addUploadElement($elementName) {
6a488035
TO
1691 $uploadNames = $this->get('uploadNames');
1692 if (!$uploadNames) {
1693 $uploadNames = array();
1694 }
1695 if (is_array($elementName)) {
1696 foreach ($elementName as $name) {
1697 if (!in_array($name, $uploadNames)) {
1698 $uploadNames[] = $name;
1699 }
1700 }
1701 }
1702 else {
1703 if (!in_array($elementName, $uploadNames)) {
1704 $uploadNames[] = $elementName;
1705 }
1706 }
1707 $this->set('uploadNames', $uploadNames);
1708
1709 $config = CRM_Core_Config::singleton();
1710 if (!empty($uploadNames)) {
1711 $this->controller->addUploadAction($config->customFileUploadDir, $uploadNames);
1712 }
1713 }
1714
a0ee3941
EM
1715 /**
1716 * @param $name
1717 *
1718 * @return null
1719 */
00be9182 1720 public function getVar($name) {
6a488035
TO
1721 return isset($this->$name) ? $this->$name : NULL;
1722 }
1723
a0ee3941
EM
1724 /**
1725 * @param $name
1726 * @param $value
1727 */
00be9182 1728 public function setVar($name, $value) {
6a488035
TO
1729 $this->$name = $value;
1730 }
1731
1732 /**
fe482240 1733 * Add date.
6a488035 1734 *
013ac5df
CW
1735 * @deprecated
1736 * Use $this->add('datepicker', ...) instead.
a1a2a83d
TO
1737 *
1738 * @param string $name
1739 * Name of the element.
1740 * @param string $label
1741 * Label of the element.
6a0b768e
TO
1742 * @param bool $required
1743 * True if required.
a1a2a83d
TO
1744 * @param array $attributes
1745 * Key / value pair.
6a488035 1746 */
00be9182 1747 public function addDate($name, $label, $required = FALSE, $attributes = NULL) {
a7488080 1748 if (!empty($attributes['formatType'])) {
6a488035
TO
1749 // get actual format
1750 $params = array('name' => $attributes['formatType']);
1751 $values = array();
1752
1753 // cache date information
1754 static $dateFormat;
1755 $key = "dateFormat_" . str_replace(' ', '_', $attributes['formatType']);
a7488080 1756 if (empty($dateFormat[$key])) {
6a488035
TO
1757 CRM_Core_DAO::commonRetrieve('CRM_Core_DAO_PreferencesDate', $params, $values);
1758 $dateFormat[$key] = $values;
1759 }
1760 else {
1761 $values = $dateFormat[$key];
1762 }
1763
1764 if ($values['date_format']) {
1765 $attributes['format'] = $values['date_format'];
1766 }
1767
a7488080 1768 if (!empty($values['time_format'])) {
6a488035
TO
1769 $attributes['timeFormat'] = $values['time_format'];
1770 }
1771 $attributes['startOffset'] = $values['start'];
1772 $attributes['endOffset'] = $values['end'];
1773 }
1774
1775 $config = CRM_Core_Config::singleton();
a7488080 1776 if (empty($attributes['format'])) {
6a488035
TO
1777 $attributes['format'] = $config->dateInputFormat;
1778 }
1779
1780 if (!isset($attributes['startOffset'])) {
1781 $attributes['startOffset'] = 10;
1782 }
1783
1784 if (!isset($attributes['endOffset'])) {
1785 $attributes['endOffset'] = 10;
1786 }
1787
1788 $this->add('text', $name, $label, $attributes);
1789
8cc574cf 1790 if (!empty($attributes['addTime']) || !empty($attributes['timeFormat'])) {
6a488035
TO
1791
1792 if (!isset($attributes['timeFormat'])) {
1793 $timeFormat = $config->timeInputFormat;
1794 }
1795 else {
1796 $timeFormat = $attributes['timeFormat'];
1797 }
1798
1799 // 1 - 12 hours and 2 - 24 hours, but for jquery widget it is 0 and 1 respectively
1800 if ($timeFormat) {
1801 $show24Hours = TRUE;
1802 if ($timeFormat == 1) {
1803 $show24Hours = FALSE;
1804 }
1805
1806 //CRM-6664 -we are having time element name
1807 //in either flat string or an array format.
1808 $elementName = $name . '_time';
1809 if (substr($name, -1) == ']') {
1810 $elementName = substr($name, 0, strlen($name) - 1) . '_time]';
1811 }
1812
1813 $this->add('text', $elementName, ts('Time'), array('timeFormat' => $show24Hours));
1814 }
1815 }
1816
1817 if ($required) {
1818 $this->addRule($name, ts('Please select %1', array(1 => $label)), 'required');
8cc574cf 1819 if (!empty($attributes['addTime']) && !empty($attributes['addTimeRequired'])) {
6a488035
TO
1820 $this->addRule($elementName, ts('Please enter a time.'), 'required');
1821 }
1822 }
1823 }
1824
1825 /**
013ac5df
CW
1826 * Function that will add date and time.
1827 *
1828 * @deprecated
1829 * Use $this->add('datepicker', ...) instead.
54957108 1830 *
1831 * @param string $name
1832 * @param string $label
1833 * @param bool $required
1834 * @param null $attributes
6a488035 1835 */
00be9182 1836 public function addDateTime($name, $label, $required = FALSE, $attributes = NULL) {
6a488035
TO
1837 $addTime = array('addTime' => TRUE);
1838 if (is_array($attributes)) {
1839 $attributes = array_merge($attributes, $addTime);
1840 }
1841 else {
1842 $attributes = $addTime;
1843 }
1844
1845 $this->addDate($name, $label, $required, $attributes);
1846 }
1847
1848 /**
fe482240 1849 * Add a currency and money element to the form.
3bdf1f3a 1850 *
1851 * @param string $name
1852 * @param string $label
1853 * @param bool $required
1854 * @param null $attributes
1855 * @param bool $addCurrency
1856 * @param string $currencyName
1857 * @param null $defaultCurrency
1858 * @param bool $freezeCurrency
1859 *
1860 * @return \HTML_QuickForm_Element
6a488035 1861 */
2da40d21 1862 public function addMoney(
f9f40af3 1863 $name,
6a488035 1864 $label,
f9f40af3
TO
1865 $required = FALSE,
1866 $attributes = NULL,
1867 $addCurrency = TRUE,
1868 $currencyName = 'currency',
6a488035 1869 $defaultCurrency = NULL,
f9f40af3 1870 $freezeCurrency = FALSE
6a488035
TO
1871 ) {
1872 $element = $this->add('text', $name, $label, $attributes, $required);
1873 $this->addRule($name, ts('Please enter a valid amount.'), 'money');
1874
1875 if ($addCurrency) {
1876 $ele = $this->addCurrency($currencyName, NULL, TRUE, $defaultCurrency, $freezeCurrency);
1877 }
1878
1879 return $element;
1880 }
1881
1882 /**
fe482240 1883 * Add currency element to the form.
54957108 1884 *
1885 * @param string $name
1886 * @param null $label
1887 * @param bool $required
1888 * @param string $defaultCurrency
1889 * @param bool $freezeCurrency
483a53a8 1890 * @param bool $setDefaultCurrency
6a488035 1891 */
2da40d21 1892 public function addCurrency(
f9f40af3
TO
1893 $name = 'currency',
1894 $label = NULL,
1895 $required = TRUE,
6a488035 1896 $defaultCurrency = NULL,
483a53a8 1897 $freezeCurrency = FALSE,
1898 $setDefaultCurrency = TRUE
6a488035
TO
1899 ) {
1900 $currencies = CRM_Core_OptionGroup::values('currencies_enabled');
91a33228 1901 if (!empty($defaultCurrency) && !array_key_exists($defaultCurrency, $currencies)) {
b740ee4b
MW
1902 Civi::log()->warning('addCurrency: Currency ' . $defaultCurrency . ' is disabled but still in use!');
1903 $currencies[$defaultCurrency] = $defaultCurrency;
1904 }
e1462487 1905 $options = array('class' => 'crm-select2 eight');
6a488035 1906 if (!$required) {
e1462487
CW
1907 $currencies = array('' => '') + $currencies;
1908 $options['placeholder'] = ts('- none -');
6a488035 1909 }
e1462487 1910 $ele = $this->add('select', $name, $label, $currencies, $required, $options);
6a488035
TO
1911 if ($freezeCurrency) {
1912 $ele->freeze();
1913 }
1914 if (!$defaultCurrency) {
1915 $config = CRM_Core_Config::singleton();
1916 $defaultCurrency = $config->defaultCurrency;
1917 }
483a53a8 1918 // In some case, setting currency field by default might override the default value
1919 // as encountered in CRM-20527 for batch data entry
1920 if ($setDefaultCurrency) {
1921 $this->setDefaults(array($name => $defaultCurrency));
1922 }
6a488035
TO
1923 }
1924
47f21f3a 1925 /**
fe482240 1926 * Create a single or multiple entity ref field.
47f21f3a
CW
1927 * @param string $name
1928 * @param string $label
6a0b768e
TO
1929 * @param array $props
1930 * Mix of html and widget properties, including:.
16b10e64
CW
1931 * - select - params to give to select2 widget
1932 * - entity - defaults to contact
1933 * - create - can the user create a new entity on-the-fly?
79ae07d9 1934 * Set to TRUE if entity is contact and you want the default profiles,
b9aa8f56 1935 * or pass in your own set of links. @see CRM_Core_BAO_UFGroup::getCreateLinks for format
353ea873 1936 * note that permissions are checked automatically
16b10e64 1937 * - api - array of settings for the getlist api wrapper
353ea873 1938 * note that it accepts a 'params' setting which will be passed to the underlying api
16b10e64
CW
1939 * - placeholder - string
1940 * - multiple - bool
1941 * - class, etc. - other html properties
fd36866a 1942 * @param bool $required
79ae07d9 1943 *
47f21f3a
CW
1944 * @return HTML_QuickForm_Element
1945 */
00be9182 1946 public function addEntityRef($name, $label = '', $props = array(), $required = FALSE) {
a88cf11a 1947 require_once "api/api.php";
c66581f5 1948 $config = CRM_Core_Config::singleton();
76ec9ca7 1949 // Default properties
704d3260 1950 $props['api'] = CRM_Utils_Array::value('api', $props, array());
a88cf11a
CW
1951 $props['entity'] = _civicrm_api_get_entity_name_from_camel(CRM_Utils_Array::value('entity', $props, 'contact'));
1952 $props['class'] = ltrim(CRM_Utils_Array::value('class', $props, '') . ' crm-form-entityref');
47f21f3a 1953
8dbd6052
CW
1954 if (isset($props['create']) && $props['create'] === TRUE) {
1955 require_once "api/v3/utils.php";
1956 $baoClass = _civicrm_api3_get_BAO($props['entity']);
1957 $props['create'] = $baoClass && is_callable([$baoClass, 'entityRefCreateLinks']) ? $baoClass::entityRefCreateLinks() : FALSE;
1958 }
1959 if (array_key_exists('create', $props) && empty($props['create'])) {
79ae07d9
CW
1960 unset($props['create']);
1961 }
79ae07d9 1962
a88cf11a
CW
1963 $props['placeholder'] = CRM_Utils_Array::value('placeholder', $props, $required ? ts('- select %1 -', array(1 => ts(str_replace('_', ' ', $props['entity'])))) : ts('- none -'));
1964
1965 $defaults = array();
1966 if (!empty($props['multiple'])) {
1967 $defaults['multiple'] = TRUE;
79ae07d9
CW
1968 }
1969 $props['select'] = CRM_Utils_Array::value('select', $props, array()) + $defaults;
47f21f3a 1970
f9585de5 1971 $this->formatReferenceFieldAttributes($props, get_class($this));
47f21f3a
CW
1972 return $this->add('text', $name, $label, $props, $required);
1973 }
1974
1975 /**
f9585de5 1976 * @param array $props
1977 * @param string $formName
47f21f3a 1978 */
f9585de5 1979 private function formatReferenceFieldAttributes(&$props, $formName) {
1980 CRM_Utils_Hook::alterEntityRefParams($props, $formName);
47f21f3a 1981 $props['data-select-params'] = json_encode($props['select']);
76ec9ca7
CW
1982 $props['data-api-params'] = $props['api'] ? json_encode($props['api']) : NULL;
1983 $props['data-api-entity'] = $props['entity'];
79ae07d9
CW
1984 if (!empty($props['create'])) {
1985 $props['data-create-links'] = json_encode($props['create']);
47f21f3a 1986 }
a88cf11a 1987 CRM_Utils_Array::remove($props, 'multiple', 'select', 'api', 'entity', 'create');
47f21f3a
CW
1988 }
1989
5d86176b 1990 /**
1991 * Convert all date fields within the params to mysql date ready for the
1992 * BAO layer. In this case fields are checked against the $_datefields defined for the form
1993 * and if time is defined it is incorporated
1994 *
6a0b768e
TO
1995 * @param array $params
1996 * Input params from the form.
5d86176b 1997 *
1998 * @todo it would probably be better to work on $this->_params than a passed array
1999 * @todo standardise the format which dates are passed to the BAO layer in & remove date
2000 * handling from BAO
2001 */
9b873358
TO
2002 public function convertDateFieldsToMySQL(&$params) {
2003 foreach ($this->_dateFields as $fieldName => $specs) {
2004 if (!empty($params[$fieldName])) {
5d86176b 2005 $params[$fieldName] = CRM_Utils_Date::isoToMysql(
2006 CRM_Utils_Date::processDate(
353ffa53
TO
2007 $params[$fieldName],
2008 CRM_Utils_Array::value("{$fieldName}_time", $params), TRUE)
5d86176b 2009 );
2010 }
92e4c2a5 2011 else {
9b873358 2012 if (isset($specs['default'])) {
5d86176b 2013 $params[$fieldName] = date('YmdHis', strtotime($specs['default']));
2014 }
2015 }
2016 }
2017 }
2018
a0ee3941
EM
2019 /**
2020 * @param $elementName
2021 */
00be9182 2022 public function removeFileRequiredRules($elementName) {
6a488035
TO
2023 $this->_required = array_diff($this->_required, array($elementName));
2024 if (isset($this->_rules[$elementName])) {
2025 foreach ($this->_rules[$elementName] as $index => $ruleInfo) {
2026 if ($ruleInfo['type'] == 'uploadedfile') {
2027 unset($this->_rules[$elementName][$index]);
2028 }
2029 }
2030 if (empty($this->_rules[$elementName])) {
2031 unset($this->_rules[$elementName]);
2032 }
2033 }
2034 }
2035
2036 /**
fe482240 2037 * Function that can be defined in Form to override or.
6a488035 2038 * perform specific action on cancel action
6a488035 2039 */
f9f40af3
TO
2040 public function cancelAction() {
2041 }
7cb3d4f0
CW
2042
2043 /**
fe482240 2044 * Helper function to verify that required fields have been filled.
3bdf1f3a 2045 *
7cb3d4f0 2046 * Typically called within the scope of a FormRule function
3bdf1f3a 2047 *
2048 * @param array $fields
2049 * @param array $values
2050 * @param array $errors
7cb3d4f0 2051 */
00be9182 2052 public static function validateMandatoryFields($fields, $values, &$errors) {
7cb3d4f0
CW
2053 foreach ($fields as $name => $fld) {
2054 if (!empty($fld['is_required']) && CRM_Utils_System::isNull(CRM_Utils_Array::value($name, $values))) {
2055 $errors[$name] = ts('%1 is a required field.', array(1 => $fld['title']));
2056 }
2057 }
2058 }
da8d9879 2059
aa1b1481
EM
2060 /**
2061 * Get contact if for a form object. Prioritise
16b10e64 2062 * - cid in URL if 0 (on behalf on someoneelse)
aa1b1481 2063 * (@todo consider setting a variable if onbehalf for clarity of downstream 'if's
16b10e64
CW
2064 * - logged in user id if it matches the one in the cid in the URL
2065 * - contact id validated from a checksum from a checksum
2066 * - cid from the url if the caller has ACL permission to view
2067 * - fallback is logged in user (or ? NULL if no logged in user) (@todo wouldn't 0 be more intuitive?)
aa1b1481 2068 *
5c766a0b 2069 * @return NULL|int
aa1b1481 2070 */
8d388047 2071 protected function setContactID() {
da8d9879 2072 $tempID = CRM_Utils_Request::retrieve('cid', 'Positive', $this);
7b4d7ab8 2073 if (isset($this->_params) && !empty($this->_params['select_contact_id'])) {
596bff78 2074 $tempID = $this->_params['select_contact_id'];
2075 }
22e263ad 2076 if (isset($this->_params, $this->_params[0]) && !empty($this->_params[0]['select_contact_id'])) {
e1ce628e 2077 // event form stores as an indexed array, contribution form not so much...
2078 $tempID = $this->_params[0]['select_contact_id'];
2079 }
c156d4d6 2080
da8d9879 2081 // force to ignore the authenticated user
c156d4d6
E
2082 if ($tempID === '0' || $tempID === 0) {
2083 // we set the cid on the form so that this will be retained for the Confirm page
2084 // in the multi-page form & prevent us returning the $userID when this is called
2085 // from that page
2086 // we don't really need to set it when $tempID is set because the params have that stored
2087 $this->set('cid', 0);
11cf613f 2088 CRM_Core_Resources::singleton()->addVars('coreForm', array('contact_id' => (int) $tempID));
aa288d3f 2089 return (int) $tempID;
da8d9879
DG
2090 }
2091
596bff78 2092 $userID = $this->getLoggedInUserContactID();
da8d9879 2093
18406494 2094 if (!is_null($tempID) && $tempID === $userID) {
11cf613f 2095 CRM_Core_Resources::singleton()->addVars('coreForm', array('contact_id' => (int) $tempID));
aa288d3f 2096 return (int) $userID;
da8d9879
DG
2097 }
2098
2099 //check if this is a checksum authentication
2100 $userChecksum = CRM_Utils_Request::retrieve('cs', 'String', $this);
2101 if ($userChecksum) {
2102 //check for anonymous user.
2103 $validUser = CRM_Contact_BAO_Contact_Utils::validChecksum($tempID, $userChecksum);
2104 if ($validUser) {
11cf613f 2105 CRM_Core_Resources::singleton()->addVars('coreForm', array('contact_id' => (int) $tempID));
c9bb1b9f 2106 CRM_Core_Resources::singleton()->addVars('coreForm', array('checksum' => $userChecksum));
da8d9879
DG
2107 return $tempID;
2108 }
2109 }
2110 // check if user has permission, CRM-12062
4c9b6178 2111 elseif ($tempID && CRM_Contact_BAO_Contact_Permission::allow($tempID)) {
11cf613f 2112 CRM_Core_Resources::singleton()->addVars('coreForm', array('contact_id' => (int) $tempID));
da8d9879
DG
2113 return $tempID;
2114 }
064af727 2115 if (is_numeric($userID)) {
2116 CRM_Core_Resources::singleton()->addVars('coreForm', array('contact_id' => (int) $userID));
2117 }
f03d4901 2118 return is_numeric($userID) ? $userID : NULL;
da8d9879 2119 }
596bff78 2120
3bdf1f3a 2121 /**
2122 * Get the contact id that the form is being submitted for.
2123 *
2124 * @return int|NULL
2125 */
8d388047 2126 public function getContactID() {
2127 return $this->setContactID();
2128 }
2129
f9f40af3 2130 /**
fe482240 2131 * Get the contact id of the logged in user.
f9f40af3 2132 */
00be9182 2133 public function getLoggedInUserContactID() {
596bff78 2134 // check if the user is logged in and has a contact ID
2135 $session = CRM_Core_Session::singleton();
2136 return $session->get('userID');
2137 }
2138
2139 /**
100fef9d 2140 * Add autoselector field -if user has permission to view contacts
596bff78 2141 * If adding this to a form you also need to add to the tpl e.g
2142 *
2143 * {if !empty($selectable)}
2144 * <div class="crm-summary-row">
2145 * <div class="crm-label">{$form.select_contact.label}</div>
2146 * <div class="crm-content">
2147 * {$form.select_contact.html}
2148 * </div>
2149 * </div>
2150 * {/if}
77b97be7 2151 *
6a0b768e
TO
2152 * @param array $profiles
2153 * Ids of profiles that are on the form (to be autofilled).
77b97be7
EM
2154 * @param array $autoCompleteField
2155 *
16b10e64
CW
2156 * - name_field
2157 * - id_field
2158 * - url (for ajax lookup)
596bff78 2159 *
77b97be7 2160 * @todo add data attributes so we can deal with multiple instances on a form
596bff78 2161 */
00be9182 2162 public function addAutoSelector($profiles = array(), $autoCompleteField = array()) {
596bff78 2163 $autoCompleteField = array_merge(array(
353ffa53
TO
2164 'id_field' => 'select_contact_id',
2165 'placeholder' => ts('Select someone else ...'),
2166 'show_hide' => TRUE,
2167 'api' => array('params' => array('contact_type' => 'Individual')),
2168 ), $autoCompleteField);
596bff78 2169
22e263ad 2170 if ($this->canUseAjaxContactLookups()) {
25977d86 2171 $this->assign('selectable', $autoCompleteField['id_field']);
353ffa53
TO
2172 $this->addEntityRef($autoCompleteField['id_field'], NULL, array(
2173 'placeholder' => $autoCompleteField['placeholder'],
af9b09df 2174 'api' => $autoCompleteField['api'],
353ffa53 2175 ));
596bff78 2176
96ed17aa 2177 CRM_Core_Resources::singleton()->addScriptFile('civicrm', 'js/AlternateContactSelector.js', 1, 'html-header')
353ffa53
TO
2178 ->addSetting(array(
2179 'form' => array('autocompletes' => $autoCompleteField),
2180 'ids' => array('profile' => $profiles),
2181 ));
596bff78 2182 }
2183 }
2184
dc677c00 2185 /**
dc677c00 2186 */
00be9182 2187 public function canUseAjaxContactLookups() {
dc677c00 2188 if (0 < (civicrm_api3('contact', 'getcount', array('check_permissions' => 1))) &&
353ffa53
TO
2189 CRM_Core_Permission::check(array(array('access AJAX API', 'access CiviCRM')))
2190 ) {
f9f40af3
TO
2191 return TRUE;
2192 }
dc677c00
EM
2193 }
2194
596bff78 2195 /**
2196 * Add the options appropriate to cid = zero - ie. autocomplete
2197 *
2198 * @todo there is considerable code duplication between the contribution forms & event forms. It is apparent
2199 * that small pieces of duplication are not being refactored into separate functions because their only shared parent
2200 * is this form. Inserting a class FrontEndForm.php between the contribution & event & this class would allow functions like this
2201 * and a dozen other small ones to be refactored into a shared parent with the reduction of much code duplication
7a9ab499
EM
2202 *
2203 * @param $onlinePaymentProcessorEnabled
596bff78 2204 */
00be9182 2205 public function addCIDZeroOptions($onlinePaymentProcessorEnabled) {
596bff78 2206 $this->assign('nocid', TRUE);
2207 $profiles = array();
22e263ad 2208 if ($this->_values['custom_pre_id']) {
596bff78 2209 $profiles[] = $this->_values['custom_pre_id'];
2210 }
22e263ad 2211 if ($this->_values['custom_post_id']) {
cc57909a 2212 $profiles = array_merge($profiles, (array) $this->_values['custom_post_id']);
596bff78 2213 }
22e263ad 2214 if ($onlinePaymentProcessorEnabled) {
596bff78 2215 $profiles[] = 'billing';
2216 }
22e263ad 2217 if (!empty($this->_values)) {
596bff78 2218 $this->addAutoSelector($profiles);
2219 }
2220 }
9d665938 2221
2222 /**
2223 * Set default values on form for given contact (or no contact defaults)
77b97be7 2224 *
6a0b768e
TO
2225 * @param mixed $profile_id
2226 * (can be id, or profile name).
2227 * @param int $contactID
77b97be7
EM
2228 *
2229 * @return array
9d665938 2230 */
00be9182 2231 public function getProfileDefaults($profile_id = 'Billing', $contactID = NULL) {
92e4c2a5 2232 try {
9d665938 2233 $defaults = civicrm_api3('profile', 'getsingle', array(
2234 'profile_id' => (array) $profile_id,
2235 'contact_id' => $contactID,
2236 ));
2237 return $defaults;
2238 }
2239 catch (Exception $e) {
9d665938 2240 // the try catch block gives us silent failure -not 100% sure this is a good idea
2241 // as silent failures are often worse than noisy ones
2ab5ff1d 2242 return array();
9d665938 2243 }
2244 }
cae80d9f
CW
2245
2246 /**
fe482240 2247 * Sets form attribute.
cae80d9f
CW
2248 * @see CRM.loadForm
2249 */
00be9182 2250 public function preventAjaxSubmit() {
cae80d9f
CW
2251 $this->setAttribute('data-no-ajax-submit', 'true');
2252 }
2253
2254 /**
fe482240 2255 * Sets form attribute.
cae80d9f
CW
2256 * @see CRM.loadForm
2257 */
00be9182 2258 public function allowAjaxSubmit() {
cae80d9f
CW
2259 $this->removeAttribute('data-no-ajax-submit');
2260 }
e2046b33
CW
2261
2262 /**
fe482240 2263 * Sets page title based on entity and action.
e2046b33
CW
2264 * @param string $entityLabel
2265 */
00be9182 2266 public function setPageTitle($entityLabel) {
e2046b33
CW
2267 switch ($this->_action) {
2268 case CRM_Core_Action::ADD:
2269 CRM_Utils_System::setTitle(ts('New %1', array(1 => $entityLabel)));
2270 break;
f9f40af3 2271
e2046b33
CW
2272 case CRM_Core_Action::UPDATE:
2273 CRM_Utils_System::setTitle(ts('Edit %1', array(1 => $entityLabel)));
2274 break;
f9f40af3 2275
e2046b33
CW
2276 case CRM_Core_Action::VIEW:
2277 case CRM_Core_Action::PREVIEW:
2278 CRM_Utils_System::setTitle(ts('View %1', array(1 => $entityLabel)));
2279 break;
f9f40af3 2280
e2046b33
CW
2281 case CRM_Core_Action::DELETE:
2282 CRM_Utils_System::setTitle(ts('Delete %1', array(1 => $entityLabel)));
2283 break;
2284 }
2285 }
1d07e7ab
CW
2286
2287 /**
2288 * Create a chain-select target field. All settings are optional; the defaults usually work.
2289 *
2290 * @param string $elementName
2291 * @param array $settings
2292 *
2293 * @return HTML_QuickForm_Element
2294 */
2295 public function addChainSelect($elementName, $settings = array()) {
2296 $props = $settings += array(
353ffa53
TO
2297 'control_field' => str_replace(array('state_province', 'StateProvince', 'county', 'County'), array(
2298 'country',
2299 'Country',
2300 'state_province',
af9b09df 2301 'StateProvince',
353ffa53 2302 ), $elementName),
1d07e7ab 2303 'data-callback' => strpos($elementName, 'rovince') ? 'civicrm/ajax/jqState' : 'civicrm/ajax/jqCounty',
757069de 2304 'label' => strpos($elementName, 'rovince') ? ts('State/Province') : ts('County'),
1d07e7ab
CW
2305 'data-empty-prompt' => strpos($elementName, 'rovince') ? ts('Choose country first') : ts('Choose state first'),
2306 'data-none-prompt' => ts('- N/A -'),
2307 'multiple' => FALSE,
2308 'required' => FALSE,
2309 'placeholder' => empty($settings['required']) ? ts('- none -') : ts('- select -'),
2310 );
b248d52b 2311 CRM_Utils_Array::remove($props, 'label', 'required', 'control_field', 'context');
8f9c3cbe 2312 $props['class'] = (empty($props['class']) ? '' : "{$props['class']} ") . 'crm-select2';
1d07e7ab
CW
2313 $props['data-select-prompt'] = $props['placeholder'];
2314 $props['data-name'] = $elementName;
2315
2316 $this->_chainSelectFields[$settings['control_field']] = $elementName;
2317
6a6ab43a
CW
2318 // Passing NULL instead of an array of options
2319 // CRM-15225 - normally QF will reject any selected values that are not part of the field's options, but due to a
2320 // quirk in our patched version of HTML_QuickForm_select, this doesn't happen if the options are NULL
2321 // which seems a bit dirty but it allows our dynamically-popuplated select element to function as expected.
c46f87cf 2322 return $this->add('select', $elementName, $settings['label'], NULL, $settings['required'], $props);
1d07e7ab
CW
2323 }
2324
87ecd5b7 2325 /**
2326 * Add actions menu to results form.
2327 *
c794f667 2328 * @param array $tasks
87ecd5b7 2329 */
2330 public function addTaskMenu($tasks) {
2331 if (is_array($tasks) && !empty($tasks)) {
1a7356e7 2332 // Set constants means this will always load with an empty value, not reloading any submitted value.
2333 // This is appropriate as it is a pseudofield.
2334 $this->setConstants(array('task' => ''));
44543184 2335 $this->assign('taskMetaData', $tasks);
2336 $select = $this->add('select', 'task', NULL, array('' => ts('Actions')), FALSE, array(
2337 'class' => 'crm-select2 crm-action-menu fa-check-circle-o huge crm-search-result-actions')
2338 );
2339 foreach ($tasks as $key => $task) {
2340 $attributes = array();
1a7356e7 2341 if (isset($task['data'])) {
2342 foreach ($task['data'] as $dataKey => $dataValue) {
2343 $attributes['data-' . $dataKey] = $dataValue;
2344 }
44543184 2345 }
2346 $select->addOption($task['title'], $key, $attributes);
2347 }
87ecd5b7 2348 if (empty($this->_actionButtonName)) {
2349 $this->_actionButtonName = $this->getButtonName('next', 'action');
2350 }
2351 $this->assign('actionButtonName', $this->_actionButtonName);
2352 $this->add('submit', $this->_actionButtonName, ts('Go'), array('class' => 'hiddenElement crm-search-go-button'));
2353
2354 // Radio to choose "All items" or "Selected items only"
2355 $selectedRowsRadio = $this->addElement('radio', 'radio_ts', NULL, '', 'ts_sel', array('checked' => 'checked'));
2356 $allRowsRadio = $this->addElement('radio', 'radio_ts', NULL, '', 'ts_all');
2357 $this->assign('ts_sel_id', $selectedRowsRadio->_attributes['id']);
2358 $this->assign('ts_all_id', $allRowsRadio->_attributes['id']);
2359
2360 CRM_Core_Resources::singleton()->addScriptFile('civicrm', 'js/crm.searchForm.js', 1, 'html-header');
2361 }
2362 }
2363
1d07e7ab
CW
2364 /**
2365 * Set options and attributes for chain select fields based on the controlling field's value
2366 */
2367 private function preProcessChainSelectFields() {
2368 foreach ($this->_chainSelectFields as $control => $target) {
a3984622
OB
2369 // The 'target' might get missing if extensions do removeElement() in a form hook.
2370 if ($this->elementExists($target)) {
2371 $targetField = $this->getElement($target);
2372 $targetType = $targetField->getAttribute('data-callback') == 'civicrm/ajax/jqCounty' ? 'county' : 'stateProvince';
2373 $options = array();
2374 // If the control field is on the form, setup chain-select and dynamically populate options
2375 if ($this->elementExists($control)) {
2376 $controlField = $this->getElement($control);
2377 $controlType = $targetType == 'county' ? 'stateProvince' : 'country';
2378
2379 $targetField->setAttribute('class', $targetField->getAttribute('class') . ' crm-chain-select-target');
2380
2381 $css = (string) $controlField->getAttribute('class');
2382 $controlField->updateAttributes(array(
2383 'class' => ($css ? "$css " : 'crm-select2 ') . 'crm-chain-select-control',
2384 'data-target' => $target,
2385 ));
2386 $controlValue = $controlField->getValue();
2387 if ($controlValue) {
2388 $options = CRM_Core_BAO_Location::getChainSelectValues($controlValue, $controlType, TRUE);
2389 if (!$options) {
2390 $targetField->setAttribute('placeholder', $targetField->getAttribute('data-none-prompt'));
2391 }
4a44fd8a 2392 }
b71cb966 2393 else {
a3984622
OB
2394 $targetField->setAttribute('placeholder', $targetField->getAttribute('data-empty-prompt'));
2395 $targetField->setAttribute('disabled', 'disabled');
8f9c3cbe 2396 }
0db6c3e1 2397 }
a3984622 2398 // Control field not present - fall back to loading default options
0db6c3e1 2399 else {
a3984622 2400 $options = CRM_Core_PseudoConstant::$targetType();
1d07e7ab 2401 }
a3984622
OB
2402 if (!$targetField->getAttribute('multiple')) {
2403 $options = array('' => $targetField->getAttribute('placeholder')) + $options;
2404 $targetField->removeAttribute('placeholder');
2405 }
2406 $targetField->_options = array();
2407 $targetField->loadArray($options);
1d07e7ab 2408 }
1d07e7ab
CW
2409 }
2410 }
bc999cd1
CW
2411
2412 /**
2413 * Validate country / state / county match and suppress unwanted "required" errors
2414 */
2415 private function validateChainSelectFields() {
2416 foreach ($this->_chainSelectFields as $control => $target) {
a3984622 2417 if ($this->elementExists($control) && $this->elementExists($target)) {
f9f40af3 2418 $controlValue = (array) $this->getElementValue($control);
14b2ff15
CW
2419 $targetField = $this->getElement($target);
2420 $controlType = $targetField->getAttribute('data-callback') == 'civicrm/ajax/jqCounty' ? 'stateProvince' : 'country';
f9f40af3 2421 $targetValue = array_filter((array) $targetField->getValue());
14b2ff15
CW
2422 if ($targetValue || $this->getElementError($target)) {
2423 $options = CRM_Core_BAO_Location::getChainSelectValues($controlValue, $controlType, TRUE);
2424 if ($targetValue) {
2425 if (!array_intersect($targetValue, array_keys($options))) {
2426 $this->setElementError($target, $controlType == 'country' ? ts('State/Province does not match the selected Country') : ts('County does not match the selected State/Province'));
2427 }
2428 } // Suppress "required" error for field if it has no options
2429 elseif (!$options) {
2430 $this->setElementError($target, NULL);
bc999cd1
CW
2431 }
2432 }
bc999cd1
CW
2433 }
2434 }
2435 }
96025800 2436
0b50eca0 2437 /**
2438 * Assign billing name to the template.
2439 *
2440 * @param array $params
2441 * Form input params, default to $this->_params.
f3f00653 2442 *
2443 * @return string
0b50eca0 2444 */
2445 public function assignBillingName($params = array()) {
2446 $name = '';
2447 if (empty($params)) {
2448 $params = $this->_params;
2449 }
2450 if (!empty($params['billing_first_name'])) {
2451 $name = $params['billing_first_name'];
2452 }
2453
2454 if (!empty($params['billing_middle_name'])) {
2455 $name .= " {$params['billing_middle_name']}";
2456 }
2457
2458 if (!empty($params['billing_last_name'])) {
2459 $name .= " {$params['billing_last_name']}";
2460 }
2461 $name = trim($name);
2462 $this->assign('billingName', $name);
2463 return $name;
2464 }
2465
fd0770bc 2466 /**
2467 * Get the currency for the form.
2468 *
2469 * @todo this should be overriden on the forms rather than having this
2470 * historic, possible handling in here. As we clean that up we should
2471 * add deprecation notices into here.
e9bb043a 2472 *
2473 * @param array $submittedValues
2474 * Array allowed so forms inheriting this class do not break.
2475 * Ideally we would make a clear standard around how submitted values
2476 * are stored (is $this->_values consistently doing that?).
2477 *
2478 * @return string
fd0770bc 2479 */
e9bb043a 2480 public function getCurrency($submittedValues = array()) {
fd0770bc 2481 $currency = CRM_Utils_Array::value('currency', $this->_values);
2482 // For event forms, currency is in a different spot
2483 if (empty($currency)) {
2484 $currency = CRM_Utils_Array::value('currency', CRM_Utils_Array::value('event', $this->_values));
2485 }
2486 if (empty($currency)) {
2487 $currency = CRM_Utils_Request::retrieveValue('currency', 'String');
2488 }
2489 // @todo If empty there is a problem - we should probably put in a deprecation notice
2490 // to warn if that seems to be happening.
2491 return $currency;
2492 }
2493
240b0e65 2494 /**
2495 * Is the form in view or edit mode.
2496 *
2497 * The 'addField' function relies on the form action being one of a set list
2498 * of actions. Checking for these allows for an early return.
2499 *
2500 * @return bool
2501 */
2502 protected function isFormInViewOrEditMode() {
2503 return in_array($this->_action, [
2504 CRM_Core_Action::UPDATE,
2505 CRM_Core_Action::ADD,
2506 CRM_Core_Action::VIEW,
2507 CRM_Core_Action::BROWSE,
2508 CRM_Core_Action::BASIC,
2509 CRM_Core_Action::ADVANCED,
2510 CRM_Core_Action::PREVIEW,
2511 ]);
2512 }
2513
6a488035 2514}