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