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