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