CRM-17154 fix search actions to support refreshing fields in confirm (like save a...
[civicrm-core.git] / CRM / Core / Form.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.7 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2016 |
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 +--------------------------------------------------------------------+
26 */
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
34 * @copyright CiviCRM LLC (c) 2004-2016
35 */
36
37 require_once 'HTML/QuickForm/Page.php';
38
39 /**
40 * Class CRM_Core_Form
41 */
42 class 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
62 /**
63 * The default values for the form.
64 *
65 * @var array
66 */
67 public $_defaults = array();
68
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
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 */
90 protected $_paymentProcessors;
91
92 /**
93 * Available payment processors (IDS).
94 *
95 * As part of trying to consolidate various payment pages we store processors here & have functions
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.
98 *
99 * @var array
100 * An array of the IDS available on this form.
101 */
102 public $_paymentProcessorIDs;
103
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
126 /**
127 * The renderer used for this form
128 *
129 * @var object
130 */
131 protected $_renderer;
132
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
148 /**
149 * Cache the smarty template for efficiency reasons
150 *
151 * @var CRM_Core_Smarty
152 */
153 static protected $_template;
154
155 /**
156 * Indicate if this form should warn users of unsaved changes
157 */
158 protected $unsavedChangesWarn;
159
160 /**
161 * What to return to the client if in ajax mode (snippet=json)
162 *
163 * @var array
164 */
165 public $ajaxResponse = array();
166
167 /**
168 * Url path used to reach this page
169 *
170 * @var array
171 */
172 public $urlPath = array();
173
174 /**
175 * @var CRM_Core_Controller
176 */
177 public $controller;
178
179 /**
180 * Constants for attributes for various form elements
181 * attempt to standardize on the number of variations that we
182 * use of the below form elements
183 *
184 * @var const string
185 */
186 const ATTR_SPACING = '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;';
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 *
194 * @var string|int
195 */
196 const CB_PREFIX = 'mark_x_', CB_PREFIY = 'mark_y_', CB_PREFIZ = 'mark_z_', CB_PREFIX_LEN = 7;
197
198 /**
199 * @internal to keep track of chain-select fields
200 * @var array
201 */
202 private $_chainSelectFields = array();
203
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
214 /**
215 * Constructor for the basic form page.
216 *
217 * We should not use QuickForm directly. This class provides a lot
218 * of default convenient functions, rules and buttons
219 *
220 * @param object $state
221 * State associated with this form.
222 * @param \const|\enum|int $action The mode the form is operating in (None/Create/View/Update/Delete)
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.
227 *
228 * @return \CRM_Core_Form
229 */
230 public function __construct(
231 $state = NULL,
232 $action = CRM_Core_Action::NONE,
233 $method = 'post',
234 $name = NULL
235 ) {
236
237 if ($name) {
238 $this->_name = $name;
239 }
240 else {
241 // CRM-15153 - FIXME this name translates to a DOM id and is not always unique!
242 $this->_name = CRM_Utils_String::getClassName(CRM_Utils_System::getClassName($this));
243 }
244
245 $this->HTML_QuickForm_Page($this->_name, $method);
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 }
259 // Workaround for CRM-15153 - give each form a reasonably unique css class
260 $this->addClass(CRM_Utils_System::getClassName($this));
261
262 $this->assign('snippet', CRM_Utils_Array::value('snippet', $_GET));
263 }
264
265 /**
266 * Generate ID for some reason & purpose that is unknown & undocumented.
267 */
268 public static function generateID() {
269 }
270
271 /**
272 * Add one or more css classes to the form.
273 *
274 * @param string $className
275 */
276 public function addClass($className) {
277 $classes = $this->getAttribute('class');
278 $this->setAttribute('class', ($classes ? "$classes " : '') . $className);
279 }
280
281 /**
282 * Register all the standard rules that most forms potentially use.
283 */
284 public function registerRules() {
285 static $rules = array(
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',
309 'settingPath',
310 'autocomplete',
311 'validContact',
312 );
313
314 foreach ($rules as $rule) {
315 $this->registerRule($rule, 'callback', $rule, 'CRM_Utils_Rule');
316 }
317 }
318
319 /**
320 * Simple easy to use wrapper around addElement.
321 *
322 * Deal with simple validation rules.
323 *
324 * @param string $type
325 * @param string $name
326 * @param string $label
327 * @param string|array $attributes (options for select elements)
328 * @param bool $required
329 * @param array $extra
330 * (attributes for select elements).
331 *
332 * @return HTML_QuickForm_Element
333 * Could be an error object
334 */
335 public function &add(
336 $type, $name, $label = '',
337 $attributes = '', $required = FALSE, $extra = NULL
338 ) {
339 // Fudge some extra types that quickform doesn't support
340 if ($type == 'wysiwyg' || in_array($type, self::$html5Types)) {
341 $attributes = ($attributes ? $attributes : array()) + array('class' => '');
342 $attributes['class'] = ltrim($attributes['class'] . " crm-form-$type");
343 $type = $type == 'wysiwyg' ? 'textarea' : 'text';
344 }
345 // @see http://wiki.civicrm.org/confluence/display/CRMDOC/crmDatepicker
346 if ($type == 'datepicker') {
347 $attributes = ($attributes ? $attributes : array());
348 $attributes['data-crm-datepicker'] = json_encode((array) $extra);
349 $type = "text";
350 }
351 if ($type == 'select' && is_array($extra)) {
352 // Normalize this property
353 if (!empty($extra['multiple'])) {
354 $extra['multiple'] = 'multiple';
355 }
356 else {
357 unset($extra['multiple']);
358 }
359 unset($extra['size'], $extra['maxlength']);
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 }
369 }
370 $element = $this->addElement($type, $name, $label, $attributes, $extra);
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 /**
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.
395 *
396 * This is a virtual function and should be redefined if needed.
397 */
398 public function preProcess() {
399 }
400
401 /**
402 * Called after the form is validated.
403 *
404 * Any processing of form state etc should be done in this function.
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
409 */
410 public function postProcess() {
411 }
412
413 /**
414 * Main process wrapper.
415 *
416 * Implemented so that we can call all the hook functions.
417 *
418 * @param bool $allowAjax
419 * FIXME: This feels kind of hackish, ideally we would take the json-related code from this function.
420 * and bury it deeper down in the controller
421 */
422 public function mainProcess($allowAjax = TRUE) {
423 $this->postProcess();
424 $this->postProcessHook();
425
426 // Respond with JSON if in AJAX context (also support legacy value '6')
427 if ($allowAjax && !empty($_REQUEST['snippet']) && in_array($_REQUEST['snippet'], array(
428 CRM_Core_Smarty::PRINT_JSON,
429 6,
430 ))
431 ) {
432 $this->ajaxResponse['buttonName'] = str_replace('_qf_' . $this->getAttribute('id') . '_', '', $this->controller->getButtonName());
433 $this->ajaxResponse['action'] = $this->_action;
434 if (isset($this->_id) || isset($this->id)) {
435 $this->ajaxResponse['id'] = isset($this->id) ? $this->id : $this->_id;
436 }
437 CRM_Core_Page_AJAX::returnJsonResponse($this->ajaxResponse);
438 }
439 }
440
441 /**
442 * The postProcess hook is typically called by the framework.
443 *
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
448 */
449 public function postProcessHook() {
450 CRM_Utils_Hook::postProcess(get_class($this), $this);
451 }
452
453 /**
454 * This virtual function is used to build the form.
455 *
456 * It replaces the buildForm associated with QuickForm_Page. This allows us to put
457 * preProcess in front of the actual form building routine
458 */
459 public function buildQuickForm() {
460 }
461
462 /**
463 * This virtual function is used to set the default values of various form elements.
464 *
465 * @return array|NULL
466 * reference to the array of default values
467 */
468 public function setDefaultValues() {
469 return NULL;
470 }
471
472 /**
473 * This is a virtual function that adds group and global rules to the form.
474 *
475 * Keeping it distinct from the form to keep code small
476 * and localized in the form building code
477 */
478 public function addRules() {
479 }
480
481 /**
482 * Performs the server side validation.
483 * @since 1.0
484 * @return bool
485 * true if no error found
486 * @throws HTML_QuickForm_Error
487 */
488 public function validate() {
489 $error = parent::validate();
490
491 $this->validateChainSelectFields();
492
493 $hookErrors = array();
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 /**
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
514 * buildQuickForm.
515 */
516 public function buildForm() {
517 $this->_formBuilt = TRUE;
518
519 $this->preProcess();
520
521 CRM_Utils_Hook::preProcess(get_class($this), $this);
522
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);
531
532 }
533
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) {
537 $this->addElement('hidden', 'entryURL', $this->controller->_entryURL);
538 }
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
550 // also call the hook function so any modules can set their own custom defaults
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();
555
556 //Set html data-attribute to enable warning user of unsaved changes
557 if ($this->unsavedChangesWarn === TRUE
558 || (!isset($this->unsavedChangesWarn)
559 && ($this->_action & CRM_Core_Action::ADD || $this->_action & CRM_Core_Action::UPDATE)
560 )
561 ) {
562 $this->setAttribute('data-warn-changes', 'true');
563 }
564 }
565
566 /**
567 * Add default Next / Back buttons.
568 *
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.
574 */
575 public function addButtons($params) {
576 $prevnext = $spacing = array();
577 foreach ($params as $button) {
578 $attrs = array('class' => 'crm-form-submit') + (array) CRM_Utils_Array::value('js', $button);
579
580 if (!empty($button['class'])) {
581 $attrs['class'] .= ' ' . $button['class'];
582 }
583
584 if (!empty($button['isDefault'])) {
585 $attrs['class'] .= ' default';
586 }
587
588 if (in_array($button['type'], array('upload', 'next', 'submit', 'done', 'process', 'refresh'))) {
589 $attrs['class'] .= ' validate';
590 $defaultIcon = 'fa-check';
591 }
592 else {
593 $attrs['class'] .= ' cancel';
594 $defaultIcon = $button['type'] == 'back' ? 'fa-chevron-left' : 'fa-times';
595 }
596
597 if ($button['type'] === 'reset') {
598 $prevnext[] = $this->createElement($button['type'], 'reset', $button['name'], $attrs);
599 }
600 else {
601 if (!empty($button['subName'])) {
602 if ($button['subName'] == 'new') {
603 $defaultIcon = 'fa-plus-circle';
604 }
605 if ($button['subName'] == 'done') {
606 $defaultIcon = 'fa-check-circle';
607 }
608 if ($button['subName'] == 'next') {
609 $defaultIcon = 'fa-chevron-right';
610 }
611 }
612
613 if (in_array($button['type'], array('next', 'upload', 'done')) && $button['name'] === ts('Save')) {
614 $attrs['accesskey'] = 'S';
615 }
616 $icon = CRM_Utils_Array::value('icon', $button, $defaultIcon);
617 if ($icon) {
618 $attrs['crm-icon'] = $icon;
619 }
620 $buttonName = $this->getButtonName($button['type'], CRM_Utils_Array::value('subName', $button));
621 $prevnext[] = $this->createElement('submit', $buttonName, $button['name'], $attrs);
622 }
623 if (!empty($button['isDefault'])) {
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 /**
640 * Getter function for Name.
641 *
642 * @return string
643 */
644 public function getName() {
645 return $this->_name;
646 }
647
648 /**
649 * Getter function for State.
650 *
651 * @return object
652 */
653 public function &getState() {
654 return $this->_state;
655 }
656
657 /**
658 * Getter function for StateType.
659 *
660 * @return int
661 */
662 public function getStateType() {
663 return $this->_state->getType();
664 }
665
666 /**
667 * Getter function for title.
668 *
669 * Should be over-ridden by derived class.
670 *
671 * @return string
672 */
673 public function getTitle() {
674 return $this->_title ? $this->_title : ts('ERROR: Title is not Set');
675 }
676
677 /**
678 * Setter function for title.
679 *
680 * @param string $title
681 * The title of the form.
682 */
683 public function setTitle($title) {
684 $this->_title = $title;
685 }
686
687 /**
688 * Assign billing type id to bltID.
689 *
690 * @throws CRM_Core_Exception
691 */
692 public function assignBillingType() {
693 $this->_bltID = CRM_Core_BAO_LocationType::getBilling();
694 $this->set('bltID', $this->_bltID);
695 $this->assign('bltID', $this->_bltID);
696 }
697
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 *
703 * @param bool $is_pay_later_enabled
704 *
705 * @throws \CRM_Core_Exception
706 */
707 protected function assignPaymentProcessor($is_pay_later_enabled) {
708 $this->_paymentProcessors = CRM_Financial_BAO_PaymentProcessor::getPaymentProcessors(
709 array(ucfirst($this->_mode) . 'Mode'),
710 $this->_paymentProcessorIDs
711 );
712
713 if (!empty($this->_paymentProcessors)) {
714 if ($is_pay_later_enabled) {
715 $this->_paymentProcessors[0] = CRM_Financial_BAO_PaymentProcessor::getPayment(0);
716 }
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 }
732
733 }
734
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
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 }
802 if ($this->_paymentProcessorID
803 || (isset($this->_submitValues['payment_processor_id']) && $this->_submitValues['payment_processor_id'] == 0)
804 ) {
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);
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' : ''));
822 }
823
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) {
847 CRM_Core_Error::statusBounce(ts('Payment approval failed with message :') . $e->getMessage(), $payment->getCancelUrl($params['qfKey'], CRM_Utils_Array::value('participant_id', $params)));
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
856 /**
857 * Setter function for options.
858 *
859 * @param mixed $options
860 */
861 public function setOptions($options) {
862 $this->_options = $options;
863 }
864
865 /**
866 * Render form and return contents.
867 *
868 * @return string
869 */
870 public function toSmarty() {
871 $this->preProcessChainSelectFields();
872 $renderer = $this->getRenderer();
873 $this->accept($renderer);
874 $content = $renderer->toArray();
875 $content['formName'] = $this->getName();
876 // CRM-15153
877 $content['formClass'] = CRM_Utils_System::getClassName($this);
878 return $content;
879 }
880
881 /**
882 * Getter function for renderer.
883 *
884 * If renderer is not set create one and initialize it.
885 *
886 * @return object
887 */
888 public function &getRenderer() {
889 if (!isset($this->_renderer)) {
890 $this->_renderer = CRM_Core_Form_Renderer::singleton();
891 }
892 return $this->_renderer;
893 }
894
895 /**
896 * Use the form name to create the tpl file name.
897 *
898 * @return string
899 */
900 public function getTemplateFileName() {
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 {
907 $tplname = strtr(
908 CRM_Utils_System::getClassName($this),
909 array(
910 '_' => DIRECTORY_SEPARATOR,
911 '\\' => DIRECTORY_SEPARATOR,
912 )
913 ) . '.tpl';
914 }
915 return $tplname;
916 }
917
918 /**
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.
922 */
923 public function getHookedTemplateFileName() {
924 $pageTemplateFile = $this->getTemplateFileName();
925 CRM_Utils_Hook::alterTemplateFile(get_class($this), $this, 'page', $pageTemplateFile);
926 return $pageTemplateFile;
927 }
928
929 /**
930 * Default extra tpl file basically just replaces .tpl with .extra.tpl.
931 *
932 * i.e. we do not override.
933 *
934 * @return string
935 */
936 public function overrideExtraTemplateFileName() {
937 return NULL;
938 }
939
940 /**
941 * Error reporting mechanism.
942 *
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.
949 */
950 public function error($message, $code = NULL, $dao = NULL) {
951 if ($dao) {
952 $dao->query('ROLLBACK');
953 }
954
955 $error = CRM_Core_Error::singleton();
956
957 $error->push($code, $message);
958 }
959
960 /**
961 * Store the variable with the value in the form scope.
962 *
963 * @param string $name
964 * Name of the variable.
965 * @param mixed $value
966 * Value of the variable.
967 */
968 public function set($name, $value) {
969 $this->controller->set($name, $value);
970 }
971
972 /**
973 * Get the variable from the form scope.
974 *
975 * @param string $name
976 * Name of the variable
977 *
978 * @return mixed
979 */
980 public function get($name) {
981 return $this->controller->get($name);
982 }
983
984 /**
985 * Getter for action.
986 *
987 * @return int
988 */
989 public function getAction() {
990 return $this->_action;
991 }
992
993 /**
994 * Setter for action.
995 *
996 * @param int $action
997 * The mode we want to set the form.
998 */
999 public function setAction($action) {
1000 $this->_action = $action;
1001 }
1002
1003 /**
1004 * Assign value to name in template.
1005 *
1006 * @param string $var
1007 * Name of variable.
1008 * @param mixed $value
1009 * Value of variable.
1010 */
1011 public function assign($var, $value = NULL) {
1012 self::$_template->assign($var, $value);
1013 }
1014
1015 /**
1016 * Assign value to name in template by reference.
1017 *
1018 * @param string $var
1019 * Name of variable.
1020 * @param mixed $value
1021 * Value of variable.
1022 */
1023 public function assign_by_ref($var, &$value) {
1024 self::$_template->assign_by_ref($var, $value);
1025 }
1026
1027 /**
1028 * Appends values to template variables.
1029 *
1030 * @param array|string $tpl_var the template variable name(s)
1031 * @param mixed $value
1032 * The value to append.
1033 * @param bool $merge
1034 */
1035 public function append($tpl_var, $value = NULL, $merge = FALSE) {
1036 self::$_template->append($tpl_var, $value, $merge);
1037 }
1038
1039 /**
1040 * Returns an array containing template variables.
1041 *
1042 * @param string $name
1043 *
1044 * @return array
1045 */
1046 public function get_template_vars($name = NULL) {
1047 return self::$_template->get_template_vars($name);
1048 }
1049
1050 /**
1051 * @param string $name
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 */
1060 public function &addRadio($name, $title, $values, $attributes = array(), $separator = NULL, $required = FALSE) {
1061 $options = array();
1062 $attributes = $attributes ? $attributes : array();
1063 $allowClear = !empty($attributes['allowClear']);
1064 unset($attributes['allowClear']);
1065 $attributes['id_suffix'] = $name;
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 }
1073 if ($allowClear) {
1074 $group->setAttribute('allowClear', TRUE);
1075 }
1076 return $group;
1077 }
1078
1079 /**
1080 * @param int $id
1081 * @param $title
1082 * @param bool $allowClear
1083 * @param null $required
1084 * @param array $attributes
1085 */
1086 public function addYesNo($id, $title, $allowClear = FALSE, $required = NULL, $attributes = array()) {
1087 $attributes += array('id_suffix' => $id);
1088 $choice = array();
1089 $choice[] = $this->createElement('radio', NULL, '11', ts('Yes'), '1', $attributes);
1090 $choice[] = $this->createElement('radio', NULL, '11', ts('No'), '0', $attributes);
1091
1092 $group = $this->addGroup($choice, $id, $title);
1093 if ($allowClear) {
1094 $group->setAttribute('allowClear', TRUE);
1095 }
1096 if ($required) {
1097 $this->addRule($id, ts('%1 is a required field.', array(1 => $title)), 'required');
1098 }
1099 }
1100
1101 /**
1102 * @param int $id
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 */
1112 public function addCheckBox(
1113 $id, $title, $values, $other = NULL,
1114 $attributes = NULL, $required = NULL,
1115 $javascriptMethod = NULL,
1116 $separator = '<br />', $flipValues = FALSE
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
1155 public function resetValues() {
1156 $data = $this->controller->container();
1157 $data['values'][$this->_name] = array();
1158 }
1159
1160 /**
1161 * Simple shell that derived classes can call to add buttons to
1162 * the form with a customized title for the main Submit
1163 *
1164 * @param string $title
1165 * Title of the main button.
1166 * @param string $nextType
1167 * Button type for the form after processing.
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
1170 */
1171 public function addDefaultButtons($title, $nextType = 'next', $backType = 'back', $submitOnce = FALSE) {
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
1193 /**
1194 * @param string $name
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 */
1202 public function addDateRange($name, $from = '_from', $to = '_to', $label = 'From:', $dateFormat = 'searchDate', $required = FALSE, $displayTime = FALSE) {
1203 if ($displayTime) {
1204 $this->addDateTime($name . $from, $label, $required, array('formatType' => $dateFormat));
1205 $this->addDateTime($name . $to, ts('To:'), $required, array('formatType' => $dateFormat));
1206 }
1207 else {
1208 $this->addDate($name . $from, $label, $required, array('formatType' => $dateFormat));
1209 $this->addDate($name . $to, ts('To:'), $required, array('formatType' => $dateFormat));
1210 }
1211 }
1212
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 }
1224 if ($action & (CRM_Core_Action::BROWSE + CRM_Core_Action::BASIC + CRM_Core_Action::ADVANCED + CRM_Core_Action::PREVIEW)) {
1225 return 'get';
1226 }
1227 // If you get this exception try adding more cases above.
1228 throw new Exception("Cannot determine api action for " . get_class($this) . '.' . 'CRM_Core_Action "' . CRM_Core_Action::description($action) . '" not recognized.');
1229 }
1230
1231 /**
1232 * Classes extending CRM_Core_Form should implement this method.
1233 * @throws Exception
1234 */
1235 public function getDefaultEntity() {
1236 throw new Exception("Cannot determine default entity. " . get_class($this) . " should implement getDefaultEntity().");
1237 }
1238
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() {
1246 throw new Exception("Cannot determine default context. " . get_class($this) . " should implement getDefaultContext().");
1247 }
1248
1249 /**
1250 * Adds a select based on field metadata.
1251 * TODO: This could be even more generic and widget type (select in this case) could also be read from metadata
1252 * Perhaps a method like $form->bind($name) which would look up all metadata for named field
1253 * @param $name
1254 * Field name to go on the form.
1255 * @param array $props
1256 * Mix of html attributes and special properties, namely.
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
1261 * - multiple - bool
1262 * - context - @see CRM_Core_DAO::buildOptionsContext
1263 * @param bool $required
1264 * @throws CRM_Core_Exception
1265 * @return HTML_QuickForm_Element
1266 */
1267 public function addSelect($name, $props = array(), $required = FALSE) {
1268 if (!isset($props['entity'])) {
1269 $props['entity'] = $this->getDefaultEntity();
1270 }
1271 if (!isset($props['field'])) {
1272 $props['field'] = strrpos($name, '[') ? rtrim(substr($name, 1 + strrpos($name, '[')), ']') : $name;
1273 }
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 }
1281 // Fetch options from the api unless passed explicitly
1282 if (isset($props['options'])) {
1283 $options = $props['options'];
1284 }
1285 else {
1286 $info = civicrm_api3($props['entity'], 'getoptions', $props);
1287 $options = $info['values'];
1288 }
1289 if (!array_key_exists('placeholder', $props)) {
1290 $props['placeholder'] = $required ? ts('- select -') : CRM_Utils_Array::value('context', $props) == 'search' ? ts('- any -') : ts('- none -');
1291 }
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);
1296 $gid = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomField', 'option_group_id', $id);
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 }
1300 }
1301 // Core field
1302 else {
1303 $info = civicrm_api3($props['entity'], 'getfields');
1304 foreach ($info['values'] as $uniqueName => $fieldSpec) {
1305 if (
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()))
1309 ) {
1310 break;
1311 }
1312 }
1313 $label = isset($props['label']) ? $props['label'] : $fieldSpec['title'];
1314 if (CRM_Utils_Array::value('context', $props) != 'search') {
1315 $props['data-option-edit-path'] = array_key_exists('option_url', $props) ? $props['option_url'] : CRM_Core_PseudoConstant::getOptionEditUrl($fieldSpec);
1316 }
1317 }
1318 $props['class'] = (isset($props['class']) ? $props['class'] . ' ' : '') . "crm-select2";
1319 $props['data-api-entity'] = $props['entity'];
1320 $props['data-api-field'] = $props['field'];
1321 CRM_Utils_Array::remove($props, 'label', 'entity', 'field', 'option_url', 'options', 'context');
1322 return $this->add('select', $name, $label, $options, $required, $props);
1323 }
1324
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)
1333 * - name (field name - only needed if different from name used on the form)
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
1339 * @throws \CiviCRM_API3_Exception
1340 * @throws \Exception
1341 * @return HTML_QuickForm_Element
1342 */
1343 public function addField($name, $props = array(), $required = FALSE) {
1344 // Resolve context.
1345 if (empty($props['context'])) {
1346 $props['context'] = $this->getDefaultContext();
1347 }
1348 $context = $props['context'];
1349 // Resolve entity.
1350 if (empty($props['entity'])) {
1351 $props['entity'] = $this->getDefaultEntity();
1352 }
1353 // Resolve field.
1354 if (empty($props['name'])) {
1355 $props['name'] = strrpos($name, '[') ? rtrim(substr($name, 1 + strrpos($name, '[')), ']') : $name;
1356 }
1357 // Resolve action.
1358 if (empty($props['action'])) {
1359 $props['action'] = $this->getApiAction();
1360 }
1361
1362 // Handle custom fields
1363 if (strpos($name, 'custom_') === 0 && is_numeric($name[7])) {
1364 $fieldId = (int) substr($name, 7);
1365 return CRM_Core_BAO_CustomField::addQuickFormElement($this, $name, $fieldId, $required, $context == 'search', CRM_Utils_Array::value('label', $props));
1366 }
1367
1368 // Core field - get metadata.
1369 $fieldSpec = civicrm_api3($props['entity'], 'getfield', $props);
1370 $fieldSpec = $fieldSpec['values'];
1371 $label = CRM_Utils_Array::value('label', $props, isset($fieldSpec['title']) ? $fieldSpec['title'] : NULL);
1372
1373 $widget = isset($props['type']) ? $props['type'] : $fieldSpec['html']['type'];
1374 if ($widget == 'TextArea' && $context == 'search') {
1375 $widget = 'Text';
1376 }
1377
1378 $isSelect = (in_array($widget, array(
1379 'Select',
1380 'CheckBoxGroup',
1381 'RadioGroup',
1382 'Radio',
1383 )));
1384
1385 if ($isSelect) {
1386 // Fetch options from the api unless passed explicitly.
1387 if (isset($props['options'])) {
1388 $options = $props['options'];
1389 }
1390 else {
1391 $options = isset($fieldSpec['options']) ? $fieldSpec['options'] : NULL;
1392 }
1393 if ($context == 'search') {
1394 $widget = 'Select';
1395 $props['multiple'] = CRM_Utils_Array::value('multiple', $props, TRUE);
1396 }
1397
1398 // Add data for popup link.
1399 if ((!empty($props['option_url']) || !array_key_exists('option_url', $props)) && ($context != 'search' && $widget == 'Select' && CRM_Core_Permission::check('administer CiviCRM'))) {
1400 $props['data-option-edit-path'] = !empty($props['option_url']) ? $props['option_url'] : CRM_Core_PseudoConstant::getOptionEditUrl($fieldSpec);
1401 $props['data-api-entity'] = $props['entity'];
1402 $props['data-api-field'] = $props['name'];
1403 }
1404 }
1405 $props += CRM_Utils_Array::value('html', $fieldSpec, array());
1406 CRM_Utils_Array::remove($props, 'entity', 'name', 'context', 'label', 'action', 'type', 'option_url', 'options');
1407
1408 // TODO: refactor switch statement, to separate methods.
1409 switch ($widget) {
1410 case 'Text':
1411 case 'Url':
1412 case 'Number':
1413 case 'Email':
1414 //TODO: Autodetect ranges
1415 $props['size'] = isset($props['size']) ? $props['size'] : 60;
1416 return $this->add(strtolower($widget), $name, $label, $props, $required);
1417
1418 case 'hidden':
1419 return $this->add('hidden', $name, NULL, $props, $required);
1420
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;
1425 return $this->add('textarea', $name, $label, $props, $required);
1426
1427 case 'Select Date':
1428 //TODO: add range support
1429 //TODO: Add date formats
1430 //TODO: Add javascript template for dates.
1431 return $this->addDate($name, $label, $required, $props);
1432
1433 case 'Radio':
1434 $separator = isset($props['separator']) ? $props['separator'] : NULL;
1435 unset($props['separator']);
1436 if (!isset($props['allowClear'])) {
1437 $props['allowClear'] = !$required;
1438 }
1439 return $this->addRadio($name, $label, $options, $props, $separator, $required);
1440
1441 case 'ChainSelect':
1442 $props += array(
1443 'required' => $required,
1444 'label' => $label,
1445 'multiple' => $context == 'search',
1446 );
1447 return $this->addChainSelect($name, $props);
1448
1449 case 'Select':
1450 $props['class'] = CRM_Utils_Array::value('class', $props, 'big') . ' crm-select2';
1451 if (!array_key_exists('placeholder', $props)) {
1452 $props['placeholder'] = $required ? ts('- select -') : ($context == 'search' ? ts('- any -') : ts('- none -'));
1453 }
1454 // TODO: Add and/or option for fields that store multiple values
1455 return $this->add('select', $name, $label, $options, $required, $props);
1456
1457 case 'CheckBoxGroup':
1458 return $this->addCheckBox($name, $label, array_flip($options), $required, $props);
1459
1460 case 'RadioGroup':
1461 return $this->addRadio($name, $label, $options, $props, NULL, $required);
1462
1463 case 'CheckBox':
1464 $text = isset($props['text']) ? $props['text'] : NULL;
1465 unset($props['text']);
1466 return $this->addElement('checkbox', $name, $label, $text, $props);
1467
1468 //add support for 'Advcheckbox' field
1469 case 'advcheckbox':
1470 $text = isset($props['text']) ? $props['text'] : NULL;
1471 unset($props['text']);
1472 return $this->addElement('advcheckbox', $name, $label, $text, $props);
1473
1474 case 'File':
1475 // We should not build upload file in search mode.
1476 if ($context == 'search') {
1477 return;
1478 }
1479 $file = $this->add('file', $name, $label, $props, $required);
1480 $this->addUploadElement($name);
1481 return $file;
1482
1483 case 'RichTextEditor':
1484 return $this->add('wysiwyg', $name, $label, $props, $required);
1485
1486 case 'EntityRef':
1487 return $this->addEntityRef($name, $label, $props, $required);
1488
1489 // Check datatypes of fields
1490 // case 'Int':
1491 //case 'Float':
1492 //case 'Money':
1493 //case read only fields
1494 default:
1495 throw new Exception("Unsupported html-element " . $widget);
1496 }
1497 }
1498
1499 /**
1500 * Add a widget for selecting/editing/creating/copying a profile form
1501 *
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.
1510 * @param array $entities
1511 * @param bool $default
1512 * //CRM-15427.
1513 * @param string $usedFor
1514 */
1515 public function addProfileSelector($name, $label, $allowCoreTypes, $allowSubTypes, $entities, $default = FALSE, $usedFor = NULL) {
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),
1525 //CRM-15427
1526 'data-default' => $default,
1527 'data-usedfor' => json_encode($usedFor),
1528 ));
1529 }
1530
1531 /**
1532 * @return null
1533 */
1534 public function getRootTitle() {
1535 return NULL;
1536 }
1537
1538 /**
1539 * @return string
1540 */
1541 public function getCompleteTitle() {
1542 return $this->getRootTitle() . $this->getTitle();
1543 }
1544
1545 /**
1546 * @return CRM_Core_Smarty
1547 */
1548 public static function &getTemplate() {
1549 return self::$_template;
1550 }
1551
1552 /**
1553 * @param $elementName
1554 */
1555 public function addUploadElement($elementName) {
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
1580 /**
1581 * @param $name
1582 *
1583 * @return null
1584 */
1585 public function getVar($name) {
1586 return isset($this->$name) ? $this->$name : NULL;
1587 }
1588
1589 /**
1590 * @param $name
1591 * @param $value
1592 */
1593 public function setVar($name, $value) {
1594 $this->$name = $value;
1595 }
1596
1597 /**
1598 * Add date.
1599 *
1600 * @deprecated
1601 * Use $this->add('datepicker', ...) instead.
1602 *
1603 * @param string $name
1604 * Name of the element.
1605 * @param string $label
1606 * Label of the element.
1607 * @param bool $required
1608 * True if required.
1609 * @param array $attributes
1610 * Key / value pair.
1611 */
1612 public function addDate($name, $label, $required = FALSE, $attributes = NULL) {
1613 if (!empty($attributes['formatType'])) {
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']);
1621 if (empty($dateFormat[$key])) {
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
1633 if (!empty($values['time_format'])) {
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();
1641 if (empty($attributes['format'])) {
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
1655 if (!empty($attributes['addTime']) || !empty($attributes['timeFormat'])) {
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');
1684 if (!empty($attributes['addTime']) && !empty($attributes['addTimeRequired'])) {
1685 $this->addRule($elementName, ts('Please enter a time.'), 'required');
1686 }
1687 }
1688 }
1689
1690 /**
1691 * Function that will add date and time.
1692 *
1693 * @deprecated
1694 * Use $this->add('datepicker', ...) instead.
1695 *
1696 * @param string $name
1697 * @param string $label
1698 * @param bool $required
1699 * @param null $attributes
1700 */
1701 public function addDateTime($name, $label, $required = FALSE, $attributes = NULL) {
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 /**
1714 * Add a currency and money element to the form.
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
1726 */
1727 public function addMoney(
1728 $name,
1729 $label,
1730 $required = FALSE,
1731 $attributes = NULL,
1732 $addCurrency = TRUE,
1733 $currencyName = 'currency',
1734 $defaultCurrency = NULL,
1735 $freezeCurrency = FALSE
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 /**
1748 * Add currency element to the form.
1749 *
1750 * @param string $name
1751 * @param null $label
1752 * @param bool $required
1753 * @param string $defaultCurrency
1754 * @param bool $freezeCurrency
1755 */
1756 public function addCurrency(
1757 $name = 'currency',
1758 $label = NULL,
1759 $required = TRUE,
1760 $defaultCurrency = NULL,
1761 $freezeCurrency = FALSE
1762 ) {
1763 $currencies = CRM_Core_OptionGroup::values('currencies_enabled');
1764 $options = array('class' => 'crm-select2 eight');
1765 if (!$required) {
1766 $currencies = array('' => '') + $currencies;
1767 $options['placeholder'] = ts('- none -');
1768 }
1769 $ele = $this->add('select', $name, $label, $currencies, $required, $options);
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
1780 /**
1781 * Create a single or multiple entity ref field.
1782 * @param string $name
1783 * @param string $label
1784 * @param array $props
1785 * Mix of html and widget properties, including:.
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?
1789 * Set to TRUE if entity is contact and you want the default profiles,
1790 * or pass in your own set of links. @see CRM_Core_BAO_UFGroup::getCreateLinks for format
1791 * note that permissions are checked automatically
1792 * - api - array of settings for the getlist api wrapper
1793 * note that it accepts a 'params' setting which will be passed to the underlying api
1794 * - placeholder - string
1795 * - multiple - bool
1796 * - class, etc. - other html properties
1797 * @param bool $required
1798 *
1799 * @return HTML_QuickForm_Element
1800 */
1801 public function addEntityRef($name, $label = '', $props = array(), $required = FALSE) {
1802 require_once "api/api.php";
1803 $config = CRM_Core_Config::singleton();
1804 // Default properties
1805 $props['api'] = CRM_Utils_Array::value('api', $props, array());
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');
1808
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 }
1812
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;
1818 }
1819 $props['select'] = CRM_Utils_Array::value('select', $props, array()) + $defaults;
1820
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']);
1830 $props['data-api-params'] = $props['api'] ? json_encode($props['api']) : NULL;
1831 $props['data-api-entity'] = $props['entity'];
1832 if (!empty($props['create'])) {
1833 $props['data-create-links'] = json_encode($props['create']);
1834 }
1835 CRM_Utils_Array::remove($props, 'multiple', 'select', 'api', 'entity', 'create');
1836 }
1837
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 *
1843 * @param array $params
1844 * Input params from the form.
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 */
1850 public function convertDateFieldsToMySQL(&$params) {
1851 foreach ($this->_dateFields as $fieldName => $specs) {
1852 if (!empty($params[$fieldName])) {
1853 $params[$fieldName] = CRM_Utils_Date::isoToMysql(
1854 CRM_Utils_Date::processDate(
1855 $params[$fieldName],
1856 CRM_Utils_Array::value("{$fieldName}_time", $params), TRUE)
1857 );
1858 }
1859 else {
1860 if (isset($specs['default'])) {
1861 $params[$fieldName] = date('YmdHis', strtotime($specs['default']));
1862 }
1863 }
1864 }
1865 }
1866
1867 /**
1868 * @param $elementName
1869 */
1870 public function removeFileRequiredRules($elementName) {
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 /**
1885 * Function that can be defined in Form to override or.
1886 * perform specific action on cancel action
1887 */
1888 public function cancelAction() {
1889 }
1890
1891 /**
1892 * Helper function to verify that required fields have been filled.
1893 *
1894 * Typically called within the scope of a FormRule function
1895 *
1896 * @param array $fields
1897 * @param array $values
1898 * @param array $errors
1899 */
1900 public static function validateMandatoryFields($fields, $values, &$errors) {
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 }
1907
1908 /**
1909 * Get contact if for a form object. Prioritise
1910 * - cid in URL if 0 (on behalf on someoneelse)
1911 * (@todo consider setting a variable if onbehalf for clarity of downstream 'if's
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?)
1916 *
1917 * @return NULL|int
1918 */
1919 protected function setContactID() {
1920 $tempID = CRM_Utils_Request::retrieve('cid', 'Positive', $this);
1921 if (isset($this->_params) && !empty($this->_params['select_contact_id'])) {
1922 $tempID = $this->_params['select_contact_id'];
1923 }
1924 if (isset($this->_params, $this->_params[0]) && !empty($this->_params[0]['select_contact_id'])) {
1925 // event form stores as an indexed array, contribution form not so much...
1926 $tempID = $this->_params[0]['select_contact_id'];
1927 }
1928
1929 // force to ignore the authenticated user
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);
1936 CRM_Core_Resources::singleton()->addVars('coreForm', array('contact_id' => (int) $tempID));
1937 return (int) $tempID;
1938 }
1939
1940 $userID = $this->getLoggedInUserContactID();
1941
1942 if (!is_null($tempID) && $tempID === $userID) {
1943 CRM_Core_Resources::singleton()->addVars('coreForm', array('contact_id' => (int) $tempID));
1944 return (int) $userID;
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) {
1953 CRM_Core_Resources::singleton()->addVars('coreForm', array('contact_id' => (int) $tempID));
1954 CRM_Core_Resources::singleton()->addVars('coreForm', array('checksum' => (int) $tempID));
1955 return $tempID;
1956 }
1957 }
1958 // check if user has permission, CRM-12062
1959 elseif ($tempID && CRM_Contact_BAO_Contact_Permission::allow($tempID)) {
1960 CRM_Core_Resources::singleton()->addVars('coreForm', array('contact_id' => (int) $tempID));
1961 return $tempID;
1962 }
1963 if (is_numeric($userID)) {
1964 CRM_Core_Resources::singleton()->addVars('coreForm', array('contact_id' => (int) $userID));
1965 }
1966 return is_numeric($userID) ? $userID : NULL;
1967 }
1968
1969 /**
1970 * Get the contact id that the form is being submitted for.
1971 *
1972 * @return int|NULL
1973 */
1974 public function getContactID() {
1975 return $this->setContactID();
1976 }
1977
1978 /**
1979 * Get the contact id of the logged in user.
1980 */
1981 public function getLoggedInUserContactID() {
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 /**
1988 * Add autoselector field -if user has permission to view contacts
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}
1999 *
2000 * @param array $profiles
2001 * Ids of profiles that are on the form (to be autofilled).
2002 * @param array $autoCompleteField
2003 *
2004 * - name_field
2005 * - id_field
2006 * - url (for ajax lookup)
2007 *
2008 * @todo add data attributes so we can deal with multiple instances on a form
2009 */
2010 public function addAutoSelector($profiles = array(), $autoCompleteField = array()) {
2011 $autoCompleteField = array_merge(array(
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);
2017
2018 if ($this->canUseAjaxContactLookups()) {
2019 $this->assign('selectable', $autoCompleteField['id_field']);
2020 $this->addEntityRef($autoCompleteField['id_field'], NULL, array(
2021 'placeholder' => $autoCompleteField['placeholder'],
2022 'api' => $autoCompleteField['api'],
2023 ));
2024
2025 CRM_Core_Resources::singleton()->addScriptFile('civicrm', 'js/AlternateContactSelector.js', 1, 'html-header')
2026 ->addSetting(array(
2027 'form' => array('autocompletes' => $autoCompleteField),
2028 'ids' => array('profile' => $profiles),
2029 ));
2030 }
2031 }
2032
2033 /**
2034 */
2035 public function canUseAjaxContactLookups() {
2036 if (0 < (civicrm_api3('contact', 'getcount', array('check_permissions' => 1))) &&
2037 CRM_Core_Permission::check(array(array('access AJAX API', 'access CiviCRM')))
2038 ) {
2039 return TRUE;
2040 }
2041 }
2042
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
2050 *
2051 * @param $onlinePaymentProcessorEnabled
2052 */
2053 public function addCIDZeroOptions($onlinePaymentProcessorEnabled) {
2054 $this->assign('nocid', TRUE);
2055 $profiles = array();
2056 if ($this->_values['custom_pre_id']) {
2057 $profiles[] = $this->_values['custom_pre_id'];
2058 }
2059 if ($this->_values['custom_post_id']) {
2060 $profiles = array_merge($profiles, (array) $this->_values['custom_post_id']);
2061 }
2062 if ($onlinePaymentProcessorEnabled) {
2063 $profiles[] = 'billing';
2064 }
2065 if (!empty($this->_values)) {
2066 $this->addAutoSelector($profiles);
2067 }
2068 }
2069
2070 /**
2071 * Set default values on form for given contact (or no contact defaults)
2072 *
2073 * @param mixed $profile_id
2074 * (can be id, or profile name).
2075 * @param int $contactID
2076 *
2077 * @return array
2078 */
2079 public function getProfileDefaults($profile_id = 'Billing', $contactID = NULL) {
2080 try {
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) {
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
2090 return array();
2091 }
2092 }
2093
2094 /**
2095 * Sets form attribute.
2096 * @see CRM.loadForm
2097 */
2098 public function preventAjaxSubmit() {
2099 $this->setAttribute('data-no-ajax-submit', 'true');
2100 }
2101
2102 /**
2103 * Sets form attribute.
2104 * @see CRM.loadForm
2105 */
2106 public function allowAjaxSubmit() {
2107 $this->removeAttribute('data-no-ajax-submit');
2108 }
2109
2110 /**
2111 * Sets page title based on entity and action.
2112 * @param string $entityLabel
2113 */
2114 public function setPageTitle($entityLabel) {
2115 switch ($this->_action) {
2116 case CRM_Core_Action::ADD:
2117 CRM_Utils_System::setTitle(ts('New %1', array(1 => $entityLabel)));
2118 break;
2119
2120 case CRM_Core_Action::UPDATE:
2121 CRM_Utils_System::setTitle(ts('Edit %1', array(1 => $entityLabel)));
2122 break;
2123
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;
2128
2129 case CRM_Core_Action::DELETE:
2130 CRM_Utils_System::setTitle(ts('Delete %1', array(1 => $entityLabel)));
2131 break;
2132 }
2133 }
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(
2145 'control_field' => str_replace(array('state_province', 'StateProvince', 'county', 'County'), array(
2146 'country',
2147 'Country',
2148 'state_province',
2149 'StateProvince',
2150 ), $elementName),
2151 'data-callback' => strpos($elementName, 'rovince') ? 'civicrm/ajax/jqState' : 'civicrm/ajax/jqCounty',
2152 'label' => strpos($elementName, 'rovince') ? ts('State/Province') : ts('County'),
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 );
2159 CRM_Utils_Array::remove($props, 'label', 'required', 'control_field', 'context');
2160 $props['class'] = (empty($props['class']) ? '' : "{$props['class']} ") . 'crm-select2';
2161 $props['data-select-prompt'] = $props['placeholder'];
2162 $props['data-name'] = $elementName;
2163
2164 $this->_chainSelectFields[$settings['control_field']] = $elementName;
2165
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.
2170 return $this->add('select', $elementName, $settings['label'], NULL, $settings['required'], $props);
2171 }
2172
2173 /**
2174 * Add actions menu to results form.
2175 *
2176 * @param array $tasks
2177 */
2178 public function addTaskMenu($tasks) {
2179 if (is_array($tasks) && !empty($tasks)) {
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' => ''));
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();
2189 if (isset($task['data'])) {
2190 foreach ($task['data'] as $dataKey => $dataValue) {
2191 $attributes['data-' . $dataKey] = $dataValue;
2192 }
2193 }
2194 $select->addOption($task['title'], $key, $attributes);
2195 }
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
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) {
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 }
2240 }
2241 else {
2242 $targetField->setAttribute('placeholder', $targetField->getAttribute('data-empty-prompt'));
2243 $targetField->setAttribute('disabled', 'disabled');
2244 }
2245 }
2246 // Control field not present - fall back to loading default options
2247 else {
2248 $options = CRM_Core_PseudoConstant::$targetType();
2249 }
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);
2256 }
2257 }
2258 }
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) {
2265 if ($this->elementExists($control) && $this->elementExists($target)) {
2266 $controlValue = (array) $this->getElementValue($control);
2267 $targetField = $this->getElement($target);
2268 $controlType = $targetField->getAttribute('data-callback') == 'civicrm/ajax/jqCounty' ? 'stateProvince' : 'country';
2269 $targetValue = array_filter((array) $targetField->getValue());
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);
2279 }
2280 }
2281 }
2282 }
2283 }
2284
2285 }