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