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