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