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