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