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