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