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