CRM-16780 fix - Fatal Error on Relationship Edit page
[civicrm-core.git] / CRM / Core / Form.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.6 |
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 * $Id$
36 *
37 */
38
39 require_once 'HTML/QuickForm/Page.php';
40
41 /**
42 * Class CRM_Core_Form
43 */
44 class CRM_Core_Form extends HTML_QuickForm_Page {
45
46 /**
47 * The state object that this form belongs to
48 * @var object
49 */
50 protected $_state;
51
52 /**
53 * The name of this form
54 * @var string
55 */
56 protected $_name;
57
58 /**
59 * The title of this form
60 * @var string
61 */
62 protected $_title = NULL;
63
64 /**
65 * The options passed into this form
66 * @var mixed
67 */
68 protected $_options = NULL;
69
70 /**
71 * The mode of operation for this form
72 * @var int
73 */
74 protected $_action;
75
76 /**
77 * Available payment processors.
78 *
79 * As part of trying to consolidate various payment pages we store processors here & have functions
80 * at this level to manage them.
81 *
82 * @var array
83 * An array of payment processor details with objects loaded in the 'object' field.
84 */
85 protected $_paymentProcessors;
86
87 /**
88 * Available payment processors (IDS).
89 *
90 * As part of trying to consolidate various payment pages we store processors here & have functions
91 * at this level to manage them.
92 *
93 * @var array
94 * An array of the IDS available on this form.
95 */
96 public $_paymentProcessorIDs;
97
98 /**
99 * The renderer used for this form
100 *
101 * @var object
102 */
103 protected $_renderer;
104
105 /**
106 * An array to hold a list of datefields on the form
107 * so that they can be converted to ISO in a consistent manner
108 *
109 * @var array
110 *
111 * e.g on a form declare $_dateFields = array(
112 * 'receive_date' => array('default' => 'now'),
113 * );
114 * then in postProcess call $this->convertDateFieldsToMySQL($formValues)
115 * to have the time field re-incorporated into the field & 'now' set if
116 * no value has been passed in
117 */
118 protected $_dateFields = array();
119
120 /**
121 * Cache the smarty template for efficiency reasons
122 *
123 * @var CRM_Core_Smarty
124 */
125 static protected $_template;
126
127 /**
128 * Indicate if this form should warn users of unsaved changes
129 */
130 protected $unsavedChangesWarn;
131
132 /**
133 * What to return to the client if in ajax mode (snippet=json)
134 *
135 * @var array
136 */
137 public $ajaxResponse = array();
138
139 /**
140 * Url path used to reach this page
141 *
142 * @var array
143 */
144 public $urlPath = array();
145
146 /**
147 * @var CRM_Core_Controller
148 */
149 public $controller;
150
151 /**
152 * Constants for attributes for various form elements
153 * attempt to standardize on the number of variations that we
154 * use of the below form elements
155 *
156 * @var const string
157 */
158 const ATTR_SPACING = '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;';
159
160 /**
161 * All checkboxes are defined with a common prefix. This allows us to
162 * have the same javascript to check / clear all the checkboxes etc
163 * If u have multiple groups of checkboxes, you will need to give them different
164 * ids to avoid potential name collision
165 *
166 * @var string|int
167 */
168 const CB_PREFIX = 'mark_x_', CB_PREFIY = 'mark_y_', CB_PREFIZ = 'mark_z_', CB_PREFIX_LEN = 7;
169
170 /**
171 * @internal to keep track of chain-select fields
172 * @var array
173 */
174 private $_chainSelectFields = array();
175
176 /**
177 * Constructor for the basic form page.
178 *
179 * We should not use QuickForm directly. This class provides a lot
180 * of default convenient functions, rules and buttons
181 *
182 * @param object $state
183 * State associated with this form.
184 * @param \const|\enum|int $action The mode the form is operating in (None/Create/View/Update/Delete)
185 * @param string $method
186 * The type of http method used (GET/POST).
187 * @param string $name
188 * The name of the form if different from class name.
189 *
190 * @return \CRM_Core_Form
191 */
192 public function __construct(
193 $state = NULL,
194 $action = CRM_Core_Action::NONE,
195 $method = 'post',
196 $name = NULL
197 ) {
198
199 if ($name) {
200 $this->_name = $name;
201 }
202 else {
203 // CRM-15153 - FIXME this name translates to a DOM id and is not always unique!
204 $this->_name = CRM_Utils_String::getClassName(CRM_Utils_System::getClassName($this));
205 }
206
207 $this->HTML_QuickForm_Page($this->_name, $method);
208
209 $this->_state =& $state;
210 if ($this->_state) {
211 $this->_state->setName($this->_name);
212 }
213 $this->_action = (int) $action;
214
215 $this->registerRules();
216
217 // let the constructor initialize this, should happen only once
218 if (!isset(self::$_template)) {
219 self::$_template = CRM_Core_Smarty::singleton();
220 }
221 // Workaround for CRM-15153 - give each form a reasonably unique css class
222 $this->addClass(CRM_Utils_System::getClassName($this));
223
224 $this->assign('snippet', CRM_Utils_Array::value('snippet', $_GET));
225 }
226
227 public static function generateID() {
228 }
229
230 /**
231 * Add one or more css classes to the form
232 * @param string $className
233 */
234 public function addClass($className) {
235 $classes = $this->getAttribute('class');
236 $this->setAttribute('class', ($classes ? "$classes " : '') . $className);
237 }
238
239 /**
240 * Register all the standard rules that most forms potentially use.
241 *
242 * @return void
243 */
244 public function registerRules() {
245 static $rules = array(
246 'title',
247 'longTitle',
248 'variable',
249 'qfVariable',
250 'phone',
251 'integer',
252 'query',
253 'url',
254 'wikiURL',
255 'domain',
256 'numberOfDigit',
257 'date',
258 'currentDate',
259 'asciiFile',
260 'htmlFile',
261 'utf8File',
262 'objectExists',
263 'optionExists',
264 'postalCode',
265 'money',
266 'positiveInteger',
267 'xssString',
268 'fileExists',
269 'autocomplete',
270 'validContact',
271 );
272
273 foreach ($rules as $rule) {
274 $this->registerRule($rule, 'callback', $rule, 'CRM_Utils_Rule');
275 }
276 }
277
278 /**
279 * Simple easy to use wrapper around addElement. Deal with
280 * simple validation rules
281 *
282 * @param string $type
283 * @param string $name
284 * @param string $label
285 * @param string|array $attributes (options for select elements)
286 * @param bool $required
287 * @param array $extra
288 * (attributes for select elements).
289 *
290 * @return HTML_QuickForm_Element could be an error object
291 */
292 public function &add(
293 $type, $name, $label = '',
294 $attributes = '', $required = FALSE, $extra = NULL
295 ) {
296 if ($type == 'wysiwyg') {
297 $attributes = ($attributes ? $attributes : array()) + array('class' => '');
298 $attributes['class'] .= ' crm-form-wysiwyg';
299 $type = "textarea";
300 }
301 if ($type == 'select' && is_array($extra)) {
302 // Normalize this property
303 if (!empty($extra['multiple'])) {
304 $extra['multiple'] = 'multiple';
305 }
306 else {
307 unset($extra['multiple']);
308 }
309 // Add placeholder option for select
310 if (isset($extra['placeholder'])) {
311 if ($extra['placeholder'] === TRUE) {
312 $extra['placeholder'] = $required ? ts('- select -') : ts('- none -');
313 }
314 if (($extra['placeholder'] || $extra['placeholder'] === '') && empty($extra['multiple']) && is_array($attributes) && !isset($attributes[''])) {
315 $attributes = array('' => $extra['placeholder']) + $attributes;
316 }
317 }
318 }
319 $element = $this->addElement($type, $name, $label, $attributes, $extra);
320 if (HTML_QuickForm::isError($element)) {
321 CRM_Core_Error::fatal(HTML_QuickForm::errorMessage($element));
322 }
323
324 if ($required) {
325 if ($type == 'file') {
326 $error = $this->addRule($name, ts('%1 is a required field.', array(1 => $label)), 'uploadedfile');
327 }
328 else {
329 $error = $this->addRule($name, ts('%1 is a required field.', array(1 => $label)), 'required');
330 }
331 if (HTML_QuickForm::isError($error)) {
332 CRM_Core_Error::fatal(HTML_QuickForm::errorMessage($element));
333 }
334 }
335
336 return $element;
337 }
338
339 /**
340 * called before buildForm. Any pre-processing that
341 * needs to be done for buildForm should be done here
342 *
343 * This is a virtual function and should be redefined if needed
344 *
345 *
346 * @return void
347 */
348 public function preProcess() {
349 }
350
351 /**
352 * called after the form is validated. Any
353 * processing of form state etc should be done in this function.
354 * Typically all processing associated with a form should be done
355 * here and relevant state should be stored in the session
356 *
357 * This is a virtual function and should be redefined if needed
358 *
359 *
360 * @return void
361 */
362 public function postProcess() {
363 }
364
365 /**
366 * just a wrapper, so that we can call all the hook functions
367 * @param bool $allowAjax
368 * FIXME: This feels kind of hackish, ideally we would take the json-related code from this function.
369 * and bury it deeper down in the controller
370 */
371 public function mainProcess($allowAjax = TRUE) {
372 $this->postProcess();
373 $this->postProcessHook();
374
375 // Respond with JSON if in AJAX context (also support legacy value '6')
376 if ($allowAjax && !empty($_REQUEST['snippet']) && in_array($_REQUEST['snippet'], array(
377 CRM_Core_Smarty::PRINT_JSON,
378 6,
379 ))
380 ) {
381 $this->ajaxResponse['buttonName'] = str_replace('_qf_' . $this->getAttribute('id') . '_', '', $this->controller->getButtonName());
382 $this->ajaxResponse['action'] = $this->_action;
383 if (isset($this->_id) || isset($this->id)) {
384 $this->ajaxResponse['id'] = isset($this->id) ? $this->id : $this->_id;
385 }
386 CRM_Core_Page_AJAX::returnJsonResponse($this->ajaxResponse);
387 }
388 }
389
390 /**
391 * The postProcess hook is typically called by the framework.
392 * However in a few cases, the form exits or redirects early in which
393 * case it needs to call this function so other modules can do the needful
394 * Calling this function directly should be avoided if possible. In general a
395 * better way is to do setUserContext so the framework does the redirect
396 */
397 public function postProcessHook() {
398 CRM_Utils_Hook::postProcess(get_class($this), $this);
399 }
400
401 /**
402 * This virtual function is used to build the form. It replaces the
403 * buildForm associated with QuickForm_Page. This allows us to put
404 * preProcess in front of the actual form building routine
405 *
406 *
407 * @return void
408 */
409 public function buildQuickForm() {
410 }
411
412 /**
413 * This virtual function is used to set the default values of
414 * various form elements
415 *
416 * access public
417 *
418 * @return array|NULL
419 * reference to the array of default values
420 */
421 public function setDefaultValues() {
422 return NULL;
423 }
424
425 /**
426 * This is a virtual function that adds group and global rules to
427 * the form. Keeping it distinct from the form to keep code small
428 * and localized in the form building code
429 *
430 *
431 * @return void
432 */
433 public function addRules() {
434 }
435
436 /**
437 * Performs the server side validation.
438 * @since 1.0
439 * @return bool
440 * true if no error found
441 * @throws HTML_QuickForm_Error
442 */
443 public function validate() {
444 $error = parent::validate();
445
446 $this->validateChainSelectFields();
447
448 $hookErrors = CRM_Utils_Hook::validate(
449 get_class($this),
450 $this->_submitValues,
451 $this->_submitFiles,
452 $this
453 );
454
455 if (!is_array($hookErrors)) {
456 $hookErrors = array();
457 }
458
459 CRM_Utils_Hook::validateForm(
460 get_class($this),
461 $this->_submitValues,
462 $this->_submitFiles,
463 $this,
464 $hookErrors
465 );
466
467 if (!empty($hookErrors)) {
468 $this->_errors += $hookErrors;
469 }
470
471 return (0 == count($this->_errors));
472 }
473
474 /**
475 * Core function that builds the form. We redefine this function
476 * here and expect all CRM forms to build their form in the function
477 * buildQuickForm.
478 */
479 public function buildForm() {
480 $this->_formBuilt = TRUE;
481
482 $this->preProcess();
483
484 CRM_Utils_Hook::preProcess(get_class($this), $this);
485
486 $this->assign('translatePermission', CRM_Core_Permission::check('translate CiviCRM'));
487
488 if (
489 $this->controller->_key &&
490 $this->controller->_generateQFKey
491 ) {
492 $this->addElement('hidden', 'qfKey', $this->controller->_key);
493 $this->assign('qfKey', $this->controller->_key);
494
495 }
496
497 // _generateQFKey suppresses the qfKey generation on form snippets that
498 // are part of other forms, hence we use that to avoid adding entryURL
499 if ($this->controller->_generateQFKey && $this->controller->_entryURL) {
500 $this->addElement('hidden', 'entryURL', $this->controller->_entryURL);
501 }
502
503 $this->buildQuickForm();
504
505 $defaults = $this->setDefaultValues();
506 unset($defaults['qfKey']);
507
508 if (!empty($defaults)) {
509 $this->setDefaults($defaults);
510 }
511
512 // call the form hook
513 // also call the hook function so any modules can set their own custom defaults
514 // the user can do both the form and set default values with this hook
515 CRM_Utils_Hook::buildForm(get_class($this), $this);
516
517 $this->addRules();
518
519 //Set html data-attribute to enable warning user of unsaved changes
520 if ($this->unsavedChangesWarn === TRUE
521 || (!isset($this->unsavedChangesWarn)
522 && ($this->_action & CRM_Core_Action::ADD || $this->_action & CRM_Core_Action::UPDATE)
523 )
524 ) {
525 $this->setAttribute('data-warn-changes', 'true');
526 }
527 }
528
529 /**
530 * Add default Next / Back buttons
531 *
532 * @param array $params
533 * Array of associative arrays in the order in which the buttons should be
534 * displayed. The associate array has 3 fields: 'type', 'name' and 'isDefault'
535 * The base form class will define a bunch of static arrays for commonly used
536 * formats.
537 *
538 * @return void
539 */
540 public function addButtons($params) {
541 $prevnext = $spacing = array();
542 foreach ($params as $button) {
543 $attrs = array('class' => 'crm-form-submit') + (array) CRM_Utils_Array::value('js', $button);
544
545 if (!empty($button['class'])) {
546 $attrs['class'] .= ' ' . $button['class'];
547 }
548
549 if (!empty($button['isDefault'])) {
550 $attrs['class'] .= ' default';
551 }
552
553 if (in_array($button['type'], array('upload', 'next', 'submit', 'done', 'process', 'refresh'))) {
554 $attrs['class'] .= ' validate';
555 $defaultIcon = 'check';
556 }
557 else {
558 $attrs['class'] .= ' cancel';
559 $defaultIcon = $button['type'] == 'back' ? 'triangle-1-w' : 'close';
560 }
561
562 if ($button['type'] === 'reset') {
563 $prevnext[] = $this->createElement($button['type'], 'reset', $button['name'], $attrs);
564 }
565 else {
566 if (!empty($button['subName'])) {
567 if ($button['subName'] == 'new') {
568 $defaultIcon = 'plus';
569 }
570 if ($button['subName'] == 'done') {
571 $defaultIcon = 'circle-check';
572 }
573 if ($button['subName'] == 'next') {
574 $defaultIcon = 'circle-triangle-e';
575 }
576 }
577
578 if (in_array($button['type'], array('next', 'upload', 'done')) && $button['name'] === ts('Save')) {
579 $attrs['accesskey'] = 'S';
580 }
581 $icon = CRM_Utils_Array::value('icon', $button, $defaultIcon);
582 if ($icon) {
583 $attrs['crm-icon'] = $icon;
584 }
585 $buttonName = $this->getButtonName($button['type'], CRM_Utils_Array::value('subName', $button));
586 $prevnext[] = $this->createElement('submit', $buttonName, $button['name'], $attrs);
587 }
588 if (!empty($button['isDefault'])) {
589 $this->setDefaultAction($button['type']);
590 }
591
592 // if button type is upload, set the enctype
593 if ($button['type'] == 'upload') {
594 $this->updateAttributes(array('enctype' => 'multipart/form-data'));
595 $this->setMaxFileSize();
596 }
597
598 // hack - addGroup uses an array to express variable spacing, read from the last element
599 $spacing[] = CRM_Utils_Array::value('spacing', $button, self::ATTR_SPACING);
600 }
601 $this->addGroup($prevnext, 'buttons', '', $spacing, FALSE);
602 }
603
604 /**
605 * Getter function for Name.
606 *
607 * @return string
608 */
609 public function getName() {
610 return $this->_name;
611 }
612
613 /**
614 * Getter function for State.
615 *
616 * @return object
617 */
618 public function &getState() {
619 return $this->_state;
620 }
621
622 /**
623 * Getter function for StateType.
624 *
625 * @return int
626 */
627 public function getStateType() {
628 return $this->_state->getType();
629 }
630
631 /**
632 * Getter function for title. Should be over-ridden by derived class
633 *
634 * @return string
635 */
636 public function getTitle() {
637 return $this->_title ? $this->_title : ts('ERROR: Title is not Set');
638 }
639
640 /**
641 * Setter function for title.
642 *
643 * @param string $title
644 * The title of the form.
645 */
646 public function setTitle($title) {
647 $this->_title = $title;
648 }
649
650 /**
651 * Assign billing type id to bltID.
652 *
653 * @throws CRM_Core_Exception
654 */
655 public function assignBillingType() {
656 $this->_bltID = CRM_Core_BAO_Location::getBillingLocationId();
657 $this->set('bltID', $this->_bltID);
658 $this->assign('bltID', $this->_bltID);
659 }
660
661 /**
662 * This if a front end form function for setting the payment processor.
663 *
664 * It would be good to sync it with the back-end function on abstractEditPayment & use one everywhere.
665 *
666 * @throws \CRM_Core_Exception
667 */
668 protected function assignPaymentProcessor() {
669 $this->_paymentProcessors = CRM_Financial_BAO_PaymentProcessor::getPaymentProcessors(
670 array(ucfirst($this->_mode) . 'Mode'),
671 $this->_paymentProcessorIDs
672 );
673
674 if (!empty($this->_paymentProcessors)) {
675 foreach ($this->_paymentProcessors as $paymentProcessorID => $paymentProcessorDetail) {
676 if (empty($this->_paymentProcessor) && $paymentProcessorDetail['is_default'] == 1 || (count($this->_paymentProcessors) == 1)
677 ) {
678 $this->_paymentProcessor = $paymentProcessorDetail;
679 $this->assign('paymentProcessor', $this->_paymentProcessor);
680 // Setting this is a bit of a legacy overhang.
681 $this->_paymentObject = $paymentProcessorDetail['object'];
682 }
683 }
684 // It's not clear why we set this on the form.
685 $this->set('paymentProcessors', $this->_paymentProcessors);
686 }
687 else {
688 throw new CRM_Core_Exception(ts('A payment processor configured for this page might be disabled (contact the site administrator for assistance).'));
689 }
690 }
691
692 /**
693 * Handle Payment Processor switching for contribution and event registration forms.
694 *
695 * This function is shared between contribution & event forms & this is their common class.
696 *
697 * However, this should be seen as an in-progress refactor, the end goal being to also align the
698 * backoffice forms that action payments.
699 *
700 * This function overlaps assignPaymentProcessor, in a bad way.
701 */
702 protected function preProcessPaymentOptions() {
703 $this->_paymentProcessorID = NULL;
704 if ($this->_paymentProcessors) {
705 if (!empty($this->_submitValues)) {
706 $this->_paymentProcessorID = CRM_Utils_Array::value('payment_processor_id', $this->_submitValues);
707 $this->_paymentProcessor = CRM_Utils_Array::value($this->_paymentProcessorID, $this->_paymentProcessors);
708 $this->set('type', $this->_paymentProcessorID);
709 $this->set('mode', $this->_mode);
710 $this->set('paymentProcessor', $this->_paymentProcessor);
711 }
712 // Set default payment processor
713 else {
714 foreach ($this->_paymentProcessors as $values) {
715 if (!empty($values['is_default']) || count($this->_paymentProcessors) == 1) {
716 $this->_paymentProcessorID = $values['id'];
717 break;
718 }
719 }
720 }
721 if ($this->_paymentProcessorID) {
722 CRM_Core_Payment_ProcessorForm::preProcess($this);
723 }
724 else {
725 $this->_paymentProcessor = array();
726 }
727 CRM_Financial_Form_Payment::addCreditCardJs();
728 }
729 $this->assign('paymentProcessorID', $this->_paymentProcessorID);
730 }
731
732 /**
733 * Setter function for options.
734 *
735 * @param mixed $options
736 *
737 * @return void
738 */
739 public function setOptions($options) {
740 $this->_options = $options;
741 }
742
743 /**
744 * Getter function for link.
745 *
746 * @return string
747 */
748 public function getLink() {
749 $config = CRM_Core_Config::singleton();
750 return CRM_Utils_System::url($_GET[$config->userFrameworkURLVar],
751 '_qf_' . $this->_name . '_display=true'
752 );
753 }
754
755 /**
756 * Boolean function to determine if this is a one form page.
757 *
758 * @return bool
759 */
760 public function isSimpleForm() {
761 return $this->_state->getType() & (CRM_Core_State::START | CRM_Core_State::FINISH);
762 }
763
764 /**
765 * Getter function for Form Action.
766 *
767 * @return string
768 */
769 public function getFormAction() {
770 return $this->_attributes['action'];
771 }
772
773 /**
774 * Setter function for Form Action.
775 *
776 * @param string $action
777 *
778 * @return void
779 */
780 public function setFormAction($action) {
781 $this->_attributes['action'] = $action;
782 }
783
784 /**
785 * Render form and return contents.
786 *
787 * @return string
788 */
789 public function toSmarty() {
790 $this->preProcessChainSelectFields();
791 $renderer = $this->getRenderer();
792 $this->accept($renderer);
793 $content = $renderer->toArray();
794 $content['formName'] = $this->getName();
795 // CRM-15153
796 $content['formClass'] = CRM_Utils_System::getClassName($this);
797 return $content;
798 }
799
800 /**
801 * Getter function for renderer. If renderer is not set
802 * create one and initialize it
803 *
804 * @return object
805 */
806 public function &getRenderer() {
807 if (!isset($this->_renderer)) {
808 $this->_renderer = CRM_Core_Form_Renderer::singleton();
809 }
810 return $this->_renderer;
811 }
812
813 /**
814 * Use the form name to create the tpl file name.
815 *
816 * @return string
817 */
818 public function getTemplateFileName() {
819 $ext = CRM_Extension_System::singleton()->getMapper();
820 if ($ext->isExtensionClass(CRM_Utils_System::getClassName($this))) {
821 $filename = $ext->getTemplateName(CRM_Utils_System::getClassName($this));
822 $tplname = $ext->getTemplatePath(CRM_Utils_System::getClassName($this)) . DIRECTORY_SEPARATOR . $filename;
823 }
824 else {
825 $tplname = strtr(
826 CRM_Utils_System::getClassName($this),
827 array(
828 '_' => DIRECTORY_SEPARATOR,
829 '\\' => DIRECTORY_SEPARATOR,
830 )
831 ) . '.tpl';
832 }
833 return $tplname;
834 }
835
836 /**
837 * A wrapper for getTemplateFileName that includes calling the hook to
838 * prevent us from having to copy & paste the logic of calling the hook
839 */
840 public function getHookedTemplateFileName() {
841 $pageTemplateFile = $this->getTemplateFileName();
842 CRM_Utils_Hook::alterTemplateFile(get_class($this), $this, 'page', $pageTemplateFile);
843 return $pageTemplateFile;
844 }
845
846 /**
847 * Default extra tpl file basically just replaces .tpl with .extra.tpl
848 * i.e. we dont override
849 *
850 * @return string
851 */
852 public function overrideExtraTemplateFileName() {
853 return NULL;
854 }
855
856 /**
857 * Error reporting mechanism.
858 *
859 * @param string $message
860 * Error Message.
861 * @param int $code
862 * Error Code.
863 * @param CRM_Core_DAO $dao
864 * A data access object on which we perform a rollback if non - empty.
865 *
866 * @return void
867 */
868 public function error($message, $code = NULL, $dao = NULL) {
869 if ($dao) {
870 $dao->query('ROLLBACK');
871 }
872
873 $error = CRM_Core_Error::singleton();
874
875 $error->push($code, $message);
876 }
877
878 /**
879 * Store the variable with the value in the form scope.
880 *
881 * @param string $name
882 * Name of the variable.
883 * @param mixed $value
884 * Value of the variable.
885 *
886 * @return void
887 */
888 public function set($name, $value) {
889 $this->controller->set($name, $value);
890 }
891
892 /**
893 * Get the variable from the form scope.
894 *
895 * @param string $name
896 * Name of the variable
897 *
898 * @return mixed
899 */
900 public function get($name) {
901 return $this->controller->get($name);
902 }
903
904 /**
905 * Getter for action.
906 *
907 * @return int
908 */
909 public function getAction() {
910 return $this->_action;
911 }
912
913 /**
914 * Setter for action.
915 *
916 * @param int $action
917 * The mode we want to set the form.
918 *
919 * @return void
920 */
921 public function setAction($action) {
922 $this->_action = $action;
923 }
924
925 /**
926 * Assign value to name in template.
927 *
928 * @param string $var
929 * Name of variable.
930 * @param mixed $value
931 * Value of variable.
932 *
933 * @return void
934 */
935 public function assign($var, $value = NULL) {
936 self::$_template->assign($var, $value);
937 }
938
939 /**
940 * Assign value to name in template by reference.
941 *
942 * @param string $var
943 * Name of variable.
944 * @param mixed $value
945 * Value of varaible.
946 *
947 * @return void
948 */
949 public function assign_by_ref($var, &$value) {
950 self::$_template->assign_by_ref($var, $value);
951 }
952
953 /**
954 * Appends values to template variables.
955 *
956 * @param array|string $tpl_var the template variable name(s)
957 * @param mixed $value
958 * The value to append.
959 * @param bool $merge
960 */
961 public function append($tpl_var, $value = NULL, $merge = FALSE) {
962 self::$_template->append($tpl_var, $value, $merge);
963 }
964
965 /**
966 * Returns an array containing template variables.
967 *
968 * @param string $name
969 *
970 * @return array
971 */
972 public function get_template_vars($name = NULL) {
973 return self::$_template->get_template_vars($name);
974 }
975
976 /**
977 * @param string $name
978 * @param $title
979 * @param $values
980 * @param array $attributes
981 * @param null $separator
982 * @param bool $required
983 *
984 * @return HTML_QuickForm_group
985 */
986 public function &addRadio($name, $title, $values, $attributes = array(), $separator = NULL, $required = FALSE) {
987 $options = array();
988 $attributes = $attributes ? $attributes : array();
989 $allowClear = !empty($attributes['allowClear']);
990 unset($attributes['allowClear']);
991 $attributes['id_suffix'] = $name;
992 foreach ($values as $key => $var) {
993 $options[] = $this->createElement('radio', NULL, NULL, $var, $key, $attributes);
994 }
995 $group = $this->addGroup($options, $name, $title, $separator);
996 if ($required) {
997 $this->addRule($name, ts('%1 is a required field.', array(1 => $title)), 'required');
998 }
999 if ($allowClear) {
1000 $group->setAttribute('allowClear', TRUE);
1001 }
1002 return $group;
1003 }
1004
1005 /**
1006 * @param int $id
1007 * @param $title
1008 * @param bool $allowClear
1009 * @param null $required
1010 * @param array $attributes
1011 */
1012 public function addYesNo($id, $title, $allowClear = FALSE, $required = NULL, $attributes = array()) {
1013 $attributes += array('id_suffix' => $id);
1014 $choice = array();
1015 $choice[] = $this->createElement('radio', NULL, '11', ts('Yes'), '1', $attributes);
1016 $choice[] = $this->createElement('radio', NULL, '11', ts('No'), '0', $attributes);
1017
1018 $group = $this->addGroup($choice, $id, $title);
1019 if ($allowClear) {
1020 $group->setAttribute('allowClear', TRUE);
1021 }
1022 if ($required) {
1023 $this->addRule($id, ts('%1 is a required field.', array(1 => $title)), 'required');
1024 }
1025 }
1026
1027 /**
1028 * @param int $id
1029 * @param $title
1030 * @param $values
1031 * @param null $other
1032 * @param null $attributes
1033 * @param null $required
1034 * @param null $javascriptMethod
1035 * @param string $separator
1036 * @param bool $flipValues
1037 */
1038 public function addCheckBox(
1039 $id, $title, $values, $other = NULL,
1040 $attributes = NULL, $required = NULL,
1041 $javascriptMethod = NULL,
1042 $separator = '<br />', $flipValues = FALSE
1043 ) {
1044 $options = array();
1045
1046 if ($javascriptMethod) {
1047 foreach ($values as $key => $var) {
1048 if (!$flipValues) {
1049 $options[] = $this->createElement('checkbox', $var, NULL, $key, $javascriptMethod);
1050 }
1051 else {
1052 $options[] = $this->createElement('checkbox', $key, NULL, $var, $javascriptMethod);
1053 }
1054 }
1055 }
1056 else {
1057 foreach ($values as $key => $var) {
1058 if (!$flipValues) {
1059 $options[] = $this->createElement('checkbox', $var, NULL, $key);
1060 }
1061 else {
1062 $options[] = $this->createElement('checkbox', $key, NULL, $var);
1063 }
1064 }
1065 }
1066
1067 $this->addGroup($options, $id, $title, $separator);
1068
1069 if ($other) {
1070 $this->addElement('text', $id . '_other', ts('Other'), $attributes[$id . '_other']);
1071 }
1072
1073 if ($required) {
1074 $this->addRule($id,
1075 ts('%1 is a required field.', array(1 => $title)),
1076 'required'
1077 );
1078 }
1079 }
1080
1081 public function resetValues() {
1082 $data = $this->controller->container();
1083 $data['values'][$this->_name] = array();
1084 }
1085
1086 /**
1087 * Simple shell that derived classes can call to add buttons to
1088 * the form with a customized title for the main Submit
1089 *
1090 * @param string $title
1091 * Title of the main button.
1092 * @param string $nextType
1093 * Button type for the form after processing.
1094 * @param string $backType
1095 * @param bool|string $submitOnce If true, add javascript to next button submit which prevents it from being clicked more than once
1096 *
1097 * @return void
1098 */
1099 public function addDefaultButtons($title, $nextType = 'next', $backType = 'back', $submitOnce = FALSE) {
1100 $buttons = array();
1101 if ($backType != NULL) {
1102 $buttons[] = array(
1103 'type' => $backType,
1104 'name' => ts('Previous'),
1105 );
1106 }
1107 if ($nextType != NULL) {
1108 $nextButton = array(
1109 'type' => $nextType,
1110 'name' => $title,
1111 'isDefault' => TRUE,
1112 );
1113 if ($submitOnce) {
1114 $nextButton['js'] = array('onclick' => "return submitOnce(this,'{$this->_name}','" . ts('Processing') . "');");
1115 }
1116 $buttons[] = $nextButton;
1117 }
1118 $this->addButtons($buttons);
1119 }
1120
1121 /**
1122 * @param string $name
1123 * @param string $from
1124 * @param string $to
1125 * @param string $label
1126 * @param string $dateFormat
1127 * @param bool $required
1128 * @param bool $displayTime
1129 */
1130 public function addDateRange($name, $from = '_from', $to = '_to', $label = 'From:', $dateFormat = 'searchDate', $required = FALSE, $displayTime = FALSE) {
1131 if ($displayTime) {
1132 $this->addDateTime($name . $from, $label, $required, array('formatType' => $dateFormat));
1133 $this->addDateTime($name . $to, ts('To:'), $required, array('formatType' => $dateFormat));
1134 }
1135 else {
1136 $this->addDate($name . $from, $label, $required, array('formatType' => $dateFormat));
1137 $this->addDate($name . $to, ts('To:'), $required, array('formatType' => $dateFormat));
1138 }
1139 }
1140
1141 /**
1142 * Based on form action, return a string representing the api action.
1143 * Used by addField method.
1144 *
1145 * Return string
1146 */
1147 private function getApiAction() {
1148 $action = $this->getAction();
1149 if ($action & (CRM_Core_Action::UPDATE + CRM_Core_Action::ADD)) {
1150 return 'create';
1151 }
1152 if ($action & (CRM_Core_Action::BROWSE)) {
1153 return 'get';
1154 }
1155 // If you get this exception try adding more cases above.
1156 throw new Exception("Cannot determine api action for " . __CLASS__);
1157 }
1158
1159 /**
1160 * Classes extending CRM_Core_Form should implement this method.
1161 * @throws Exception
1162 */
1163 public function getDefaultEntity() {
1164 throw new Exception("Cannot determine default entity. The form class should implement getDefaultEntity().");
1165 }
1166
1167 /**
1168 * Classes extending CRM_Core_Form should implement this method.
1169 *
1170 * TODO: Merge with CRM_Core_DAO::buildOptionsContext($context) and add validation.
1171 * @throws Exception
1172 */
1173 public function getDefaultContext() {
1174 throw new Exception("Cannot determine default context. The form class should implement getDefaultContext().");
1175 }
1176
1177 /**
1178 * Adds a select based on field metadata.
1179 * TODO: This could be even more generic and widget type (select in this case) could also be read from metadata
1180 * Perhaps a method like $form->bind($name) which would look up all metadata for named field
1181 * @param $name
1182 * Field name to go on the form.
1183 * @param array $props
1184 * Mix of html attributes and special properties, namely.
1185 * - entity (api entity name, can usually be inferred automatically from the form class)
1186 * - field (field name - only needed if different from name used on the form)
1187 * - option_url - path to edit this option list - usually retrieved automatically - set to NULL to disable link
1188 * - placeholder - set to NULL to disable
1189 * - multiple - bool
1190 * - context - @see CRM_Core_DAO::buildOptionsContext
1191 * @param bool $required
1192 * @throws CRM_Core_Exception
1193 * @return HTML_QuickForm_Element
1194 */
1195 public function addSelect($name, $props = array(), $required = FALSE) {
1196 if (!isset($props['entity'])) {
1197 $props['entity'] = $this->getDefaultEntity();
1198 }
1199 if (!isset($props['field'])) {
1200 $props['field'] = strrpos($name, '[') ? rtrim(substr($name, 1 + strrpos($name, '[')), ']') : $name;
1201 }
1202 // Fetch options from the api unless passed explicitly
1203 if (isset($props['options'])) {
1204 $options = $props['options'];
1205 }
1206 else {
1207 $info = civicrm_api3($props['entity'], 'getoptions', $props);
1208 $options = $info['values'];
1209 }
1210 if (!array_key_exists('placeholder', $props)) {
1211 $props['placeholder'] = $required ? ts('- select -') : CRM_Utils_Array::value('context', $props) == 'search' ? ts('- any -') : ts('- none -');
1212 }
1213 // Handle custom field
1214 if (strpos($name, 'custom_') === 0 && is_numeric($name[7])) {
1215 list(, $id) = explode('_', $name);
1216 $label = isset($props['label']) ? $props['label'] : CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomField', 'label', $id);
1217 $gid = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomField', 'option_group_id', $id);
1218 if (CRM_Utils_Array::value('context', $props) != 'search') {
1219 $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);
1220 }
1221 }
1222 // Core field
1223 else {
1224 $info = civicrm_api3($props['entity'], 'getfields');
1225 foreach ($info['values'] as $uniqueName => $fieldSpec) {
1226 if (
1227 $uniqueName === $props['field'] ||
1228 CRM_Utils_Array::value('name', $fieldSpec) === $props['field'] ||
1229 in_array($props['field'], CRM_Utils_Array::value('api.aliases', $fieldSpec, array()))
1230 ) {
1231 break;
1232 }
1233 }
1234 $label = isset($props['label']) ? $props['label'] : $fieldSpec['title'];
1235 if (CRM_Utils_Array::value('context', $props) != 'search') {
1236 $props['data-option-edit-path'] = array_key_exists('option_url', $props) ? $props['option_url'] : $props['data-option-edit-path'] = CRM_Core_PseudoConstant::getOptionEditUrl($fieldSpec);
1237 }
1238 }
1239 $props['class'] = (isset($props['class']) ? $props['class'] . ' ' : '') . "crm-select2";
1240 $props['data-api-entity'] = $props['entity'];
1241 $props['data-api-field'] = $props['field'];
1242 CRM_Utils_Array::remove($props, 'label', 'entity', 'field', 'option_url', 'options', 'context');
1243 return $this->add('select', $name, $label, $options, $required, $props);
1244 }
1245
1246 /**
1247 * Adds a field based on metadata.
1248 *
1249 * @param $name
1250 * Field name to go on the form.
1251 * @param array $props
1252 * Mix of html attributes and special properties, namely.
1253 * - entity (api entity name, can usually be inferred automatically from the form class)
1254 * - name (field name - only needed if different from name used on the form)
1255 * - option_url - path to edit this option list - usually retrieved automatically - set to NULL to disable link
1256 * - placeholder - set to NULL to disable
1257 * - multiple - bool
1258 * - context - @see CRM_Core_DAO::buildOptionsContext
1259 * @param bool $required
1260 * @throws \CiviCRM_API3_Exception
1261 * @throws \Exception
1262 * @return HTML_QuickForm_Element
1263 */
1264 public function addField($name, $props = array(), $required = FALSE) {
1265 // TODO: Handle custom field
1266 if (strpos($name, 'custom_') === 0 && is_numeric($name[7])) {
1267 throw new Exception("Custom fields are not supported by the addField method. ");
1268 }
1269 // Resolve context.
1270 if (!isset($props['context'])) {
1271 $props['context'] = $this->getDefaultContext();
1272 }
1273 // Resolve entity.
1274 if (!isset($props['entity'])) {
1275 $props['entity'] = $this->getDefaultEntity();
1276 }
1277 // Resolve field.
1278 if (!isset($props['name'])) {
1279 $props['name'] = strrpos($name, '[') ? rtrim(substr($name, 1 + strrpos($name, '[')), ']') : $name;
1280 }
1281 // Resolve action.
1282 if (!isset($props['action'])) {
1283 $props['action'] = $this->getApiAction();
1284 }
1285 // Get field metadata.
1286 $fieldSpec = civicrm_api3($props['entity'], 'getfield', $props);
1287 $fieldSpec = $fieldSpec['values'];
1288 $label = CRM_Utils_Array::value('label', $props, isset($fieldSpec['title']) ? $fieldSpec['title'] : NULL);
1289
1290 $widget = isset($props['type']) ? $props['type'] : $fieldSpec['html']['type'];
1291 if ($widget == 'TextArea' && $props['context'] == 'search') {
1292 $widget = 'Text';
1293 }
1294
1295 $isSelect = (in_array($widget, array(
1296 'Select',
1297 'Multi-Select',
1298 'Select State/Province',
1299 'Multi-Select State/Province',
1300 'Select Country',
1301 'Multi-Select Country',
1302 'AdvMulti-Select',
1303 'CheckBoxGroup',
1304 'RadioGroup',
1305 'Radio',
1306 )));
1307
1308 if ($isSelect) {
1309 // Fetch options from the api unless passed explicitly.
1310 if (isset($props['options'])) {
1311 $options = $props['options'];
1312 // Else this get passed to the form->add method.
1313 unset($props['options']);
1314 }
1315 else {
1316 $options = isset($fieldSpec['options']) ? $fieldSpec['options'] : NULL;
1317 }
1318 //@TODO AdvMulti-Select is deprecated, drop support.
1319 if ($props['context'] == 'search' || ($widget !== 'AdvMulti-Select' && strpos($widget, 'Select') !== FALSE)) {
1320 $widget = 'Select';
1321 }
1322 // Set default options-url value.
1323 if ((!isset($props['options-url']))) {
1324 $props['options-url'] = TRUE;
1325 }
1326
1327 // Add data for popup link.
1328 if ((isset($props['options-url']) && $props['options-url']) && ($props['context'] != 'search' && $widget == 'Select' && CRM_Core_Permission::check('administer CiviCRM'))) {
1329 $props['data-option-edit-path'] = array_key_exists('option_url', $props) ? $props['option_url'] : $props['data-option-edit-path'] = CRM_Core_PseudoConstant::getOptionEditUrl($fieldSpec);
1330 $props['data-api-entity'] = $props['entity'];
1331 $props['data-api-field'] = $props['name'];
1332 if (isset($props['options-url'])) {
1333 unset($props['options-url']);
1334 }
1335 }
1336 }
1337 //Use select2 library for following widgets.
1338 $isSelect2 = (in_array($widget, array(
1339 'Select',
1340 'Multi-Select',
1341 'Select State/Province',
1342 'Multi-Select State/Province',
1343 'Select Country',
1344 'Multi-Select Country',
1345 )));
1346 if ($isSelect2) {
1347 $props['class'] = (isset($props['class']) ? $props['class'] . ' ' : '') . "crm-select2";
1348 if ($props['context'] == 'search' || strpos($widget, 'Multi') !== FALSE) {
1349 $props['class'] .= ' huge';
1350 $props['multiple'] = 'multiple';
1351 }
1352 // The placeholder is only used for select-elements.
1353 if (!array_key_exists('placeholder', $props)) {
1354 $props['placeholder'] = $required ? ts('- select -') : $props['context'] == 'search' ? ts('- any -') : ts('- none -');
1355 }
1356 }
1357 $props += CRM_Utils_Array::value('html', $fieldSpec, array());
1358 CRM_Utils_Array::remove($props, 'entity', 'name', 'context', 'label', 'action', 'type');
1359 // TODO: refactor switch statement, to separate methods.
1360 switch ($widget) {
1361 case 'Text':
1362 case 'Link':
1363 //TODO: Autodetect ranges
1364 $props['size'] = isset($props['size']) ? $props['size'] : 60;
1365 return $this->add('text', $name, $label, $props, $required);
1366 break;
1367
1368 case 'hidden':
1369 return $this->add('hidden', $name, NULL, $props, $required);
1370 break;
1371
1372 case 'TextArea':
1373 //Set default columns and rows for textarea.
1374 $props['rows'] = isset($props['rows']) ? $props['rows'] : 4;
1375 $props['cols'] = isset($props['cols']) ? $props['cols'] : 60;
1376 return $this->addElement('textarea', $name, $label, $props, $required);
1377 break;
1378
1379 case 'Select Date':
1380 //TODO: add range support
1381 //TODO: Add date formats
1382 //TODO: Add javascript template for dates.
1383 return $this->addDate($name, $label, $required, $props);
1384 break;
1385
1386 case 'Radio':
1387 $separator = isset($props['separator']) ? $props['separator'] : NULL;
1388 unset($props['separator']);
1389 if (!isset($props['allowClear'])) {
1390 $props['allowClear'] = !$required;
1391 }
1392 return $this->addRadio($name, $label, $options, $props, $separator, $required);
1393 break;
1394
1395 case 'Select':
1396 if (empty($props['multiple'])) {
1397 $options = array('' => $props['placeholder']) + $options;
1398 }
1399 return $this->add('select', $name, $label, $options, $required, $props);
1400 // TODO: Add and/or option for fields that store multiple values
1401 break;
1402
1403 case 'CheckBoxGroup':
1404 return $this->addCheckBox($name, $label, array_flip($options), $required, $props);
1405 break;
1406
1407 case 'RadioGroup':
1408 return $this->addRadio($name, $label, $options, $props, NULL, $required);
1409 break;
1410
1411 //case 'AdvMulti-Select':
1412 case 'CheckBox':
1413 $text = isset($props['text']) ? $props['text'] : NULL;
1414 unset($props['text']);
1415 return $this->addElement('checkbox', $name, $label, $text, $props);
1416 break;
1417
1418 case 'File':
1419 // We should not build upload file in search mode.
1420 if (isset($props['context']) && $props['context'] == 'search') {
1421 return;
1422 }
1423 $file = $this->add('file', $name, $label, $props, $required);
1424 $this->addUploadElement($name);
1425 return $file;
1426 break;
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 break;
1434
1435 // Check datatypes of fields
1436 // case 'Int':
1437 //case 'Float':
1438 //case 'Money':
1439 //case 'Link':
1440 //case read only fields
1441 default:
1442 throw new Exception("Unsupported html-element " . $widget);
1443 }
1444 }
1445
1446 /**
1447 * Add a widget for selecting/editing/creating/copying a profile form
1448 *
1449 * @param string $name
1450 * HTML form-element name.
1451 * @param string $label
1452 * Printable label.
1453 * @param string $allowCoreTypes
1454 * Only present a UFGroup if its group_type includes a subset of $allowCoreTypes; e.g. 'Individual', 'Activity'.
1455 * @param string $allowSubTypes
1456 * Only present a UFGroup if its group_type is compatible with $allowSubypes.
1457 * @param array $entities
1458 * @param bool $default
1459 * //CRM-15427.
1460 */
1461 public function addProfileSelector($name, $label, $allowCoreTypes, $allowSubTypes, $entities, $default = FALSE) {
1462 // Output widget
1463 // FIXME: Instead of adhoc serialization, use a single json_encode()
1464 CRM_UF_Page_ProfileEditor::registerProfileScripts();
1465 CRM_UF_Page_ProfileEditor::registerSchemas(CRM_Utils_Array::collect('entity_type', $entities));
1466 $this->add('text', $name, $label, array(
1467 'class' => 'crm-profile-selector',
1468 // Note: client treats ';;' as equivalent to \0, and ';;' works better in HTML
1469 'data-group-type' => CRM_Core_BAO_UFGroup::encodeGroupType($allowCoreTypes, $allowSubTypes, ';;'),
1470 'data-entities' => json_encode($entities),
1471 //CRM-15427
1472 'data-default' => $default,
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 public function getContactID() {
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 return (int) $tempID;
1901 }
1902
1903 $userID = $this->getLoggedInUserContactID();
1904
1905 if (!is_null($tempID) && $tempID === $userID) {
1906 return (int) $userID;
1907 }
1908
1909 //check if this is a checksum authentication
1910 $userChecksum = CRM_Utils_Request::retrieve('cs', 'String', $this);
1911 if ($userChecksum) {
1912 //check for anonymous user.
1913 $validUser = CRM_Contact_BAO_Contact_Utils::validChecksum($tempID, $userChecksum);
1914 if ($validUser) {
1915 return $tempID;
1916 }
1917 }
1918 // check if user has permission, CRM-12062
1919 elseif ($tempID && CRM_Contact_BAO_Contact_Permission::allow($tempID)) {
1920 return $tempID;
1921 }
1922
1923 return is_numeric($userID) ? $userID : NULL;
1924 }
1925
1926 /**
1927 * Get the contact id of the logged in user.
1928 */
1929 public function getLoggedInUserContactID() {
1930 // check if the user is logged in and has a contact ID
1931 $session = CRM_Core_Session::singleton();
1932 return $session->get('userID');
1933 }
1934
1935 /**
1936 * Add autoselector field -if user has permission to view contacts
1937 * If adding this to a form you also need to add to the tpl e.g
1938 *
1939 * {if !empty($selectable)}
1940 * <div class="crm-summary-row">
1941 * <div class="crm-label">{$form.select_contact.label}</div>
1942 * <div class="crm-content">
1943 * {$form.select_contact.html}
1944 * </div>
1945 * </div>
1946 * {/if}
1947 *
1948 * @param array $profiles
1949 * Ids of profiles that are on the form (to be autofilled).
1950 * @param array $autoCompleteField
1951 *
1952 * - name_field
1953 * - id_field
1954 * - url (for ajax lookup)
1955 *
1956 * @todo add data attributes so we can deal with multiple instances on a form
1957 */
1958 public function addAutoSelector($profiles = array(), $autoCompleteField = array()) {
1959 $autoCompleteField = array_merge(array(
1960 'id_field' => 'select_contact_id',
1961 'placeholder' => ts('Select someone else ...'),
1962 'show_hide' => TRUE,
1963 'api' => array('params' => array('contact_type' => 'Individual')),
1964 ), $autoCompleteField);
1965
1966 if ($this->canUseAjaxContactLookups()) {
1967 $this->assign('selectable', $autoCompleteField['id_field']);
1968 $this->addEntityRef($autoCompleteField['id_field'], NULL, array(
1969 'placeholder' => $autoCompleteField['placeholder'],
1970 'api' => $autoCompleteField['api'],
1971 ));
1972
1973 CRM_Core_Resources::singleton()->addScriptFile('civicrm', 'js/AlternateContactSelector.js', 1, 'html-header')
1974 ->addSetting(array(
1975 'form' => array('autocompletes' => $autoCompleteField),
1976 'ids' => array('profile' => $profiles),
1977 ));
1978 }
1979 }
1980
1981 /**
1982 */
1983 public function canUseAjaxContactLookups() {
1984 if (0 < (civicrm_api3('contact', 'getcount', array('check_permissions' => 1))) &&
1985 CRM_Core_Permission::check(array(array('access AJAX API', 'access CiviCRM')))
1986 ) {
1987 return TRUE;
1988 }
1989 }
1990
1991 /**
1992 * Add the options appropriate to cid = zero - ie. autocomplete
1993 *
1994 * @todo there is considerable code duplication between the contribution forms & event forms. It is apparent
1995 * that small pieces of duplication are not being refactored into separate functions because their only shared parent
1996 * is this form. Inserting a class FrontEndForm.php between the contribution & event & this class would allow functions like this
1997 * and a dozen other small ones to be refactored into a shared parent with the reduction of much code duplication
1998 *
1999 * @param $onlinePaymentProcessorEnabled
2000 */
2001 public function addCIDZeroOptions($onlinePaymentProcessorEnabled) {
2002 $this->assign('nocid', TRUE);
2003 $profiles = array();
2004 if ($this->_values['custom_pre_id']) {
2005 $profiles[] = $this->_values['custom_pre_id'];
2006 }
2007 if ($this->_values['custom_post_id']) {
2008 $profiles = array_merge($profiles, (array) $this->_values['custom_post_id']);
2009 }
2010 if ($onlinePaymentProcessorEnabled) {
2011 $profiles[] = 'billing';
2012 }
2013 if (!empty($this->_values)) {
2014 $this->addAutoSelector($profiles);
2015 }
2016 }
2017
2018 /**
2019 * Set default values on form for given contact (or no contact defaults)
2020 *
2021 * @param mixed $profile_id
2022 * (can be id, or profile name).
2023 * @param int $contactID
2024 *
2025 * @return array
2026 */
2027 public function getProfileDefaults($profile_id = 'Billing', $contactID = NULL) {
2028 try {
2029 $defaults = civicrm_api3('profile', 'getsingle', array(
2030 'profile_id' => (array) $profile_id,
2031 'contact_id' => $contactID,
2032 ));
2033 return $defaults;
2034 }
2035 catch (Exception $e) {
2036 // the try catch block gives us silent failure -not 100% sure this is a good idea
2037 // as silent failures are often worse than noisy ones
2038 return array();
2039 }
2040 }
2041
2042 /**
2043 * Sets form attribute.
2044 * @see CRM.loadForm
2045 */
2046 public function preventAjaxSubmit() {
2047 $this->setAttribute('data-no-ajax-submit', 'true');
2048 }
2049
2050 /**
2051 * Sets form attribute.
2052 * @see CRM.loadForm
2053 */
2054 public function allowAjaxSubmit() {
2055 $this->removeAttribute('data-no-ajax-submit');
2056 }
2057
2058 /**
2059 * Sets page title based on entity and action.
2060 * @param string $entityLabel
2061 */
2062 public function setPageTitle($entityLabel) {
2063 switch ($this->_action) {
2064 case CRM_Core_Action::ADD:
2065 CRM_Utils_System::setTitle(ts('New %1', array(1 => $entityLabel)));
2066 break;
2067
2068 case CRM_Core_Action::UPDATE:
2069 CRM_Utils_System::setTitle(ts('Edit %1', array(1 => $entityLabel)));
2070 break;
2071
2072 case CRM_Core_Action::VIEW:
2073 case CRM_Core_Action::PREVIEW:
2074 CRM_Utils_System::setTitle(ts('View %1', array(1 => $entityLabel)));
2075 break;
2076
2077 case CRM_Core_Action::DELETE:
2078 CRM_Utils_System::setTitle(ts('Delete %1', array(1 => $entityLabel)));
2079 break;
2080 }
2081 }
2082
2083 /**
2084 * Create a chain-select target field. All settings are optional; the defaults usually work.
2085 *
2086 * @param string $elementName
2087 * @param array $settings
2088 *
2089 * @return HTML_QuickForm_Element
2090 */
2091 public function addChainSelect($elementName, $settings = array()) {
2092 $props = $settings += array(
2093 'control_field' => str_replace(array('state_province', 'StateProvince', 'county', 'County'), array(
2094 'country',
2095 'Country',
2096 'state_province',
2097 'StateProvince',
2098 ), $elementName),
2099 'data-callback' => strpos($elementName, 'rovince') ? 'civicrm/ajax/jqState' : 'civicrm/ajax/jqCounty',
2100 'label' => strpos($elementName, 'rovince') ? ts('State/Province') : ts('County'),
2101 'data-empty-prompt' => strpos($elementName, 'rovince') ? ts('Choose country first') : ts('Choose state first'),
2102 'data-none-prompt' => ts('- N/A -'),
2103 'multiple' => FALSE,
2104 'required' => FALSE,
2105 'placeholder' => empty($settings['required']) ? ts('- none -') : ts('- select -'),
2106 );
2107 CRM_Utils_Array::remove($props, 'label', 'required', 'control_field');
2108 $props['class'] = (empty($props['class']) ? '' : "{$props['class']} ") . 'crm-select2';
2109 $props['data-select-prompt'] = $props['placeholder'];
2110 $props['data-name'] = $elementName;
2111
2112 $this->_chainSelectFields[$settings['control_field']] = $elementName;
2113
2114 // Passing NULL instead of an array of options
2115 // CRM-15225 - normally QF will reject any selected values that are not part of the field's options, but due to a
2116 // quirk in our patched version of HTML_QuickForm_select, this doesn't happen if the options are NULL
2117 // which seems a bit dirty but it allows our dynamically-popuplated select element to function as expected.
2118 return $this->add('select', $elementName, $settings['label'], NULL, $settings['required'], $props);
2119 }
2120
2121 /**
2122 * Set options and attributes for chain select fields based on the controlling field's value
2123 */
2124 private function preProcessChainSelectFields() {
2125 foreach ($this->_chainSelectFields as $control => $target) {
2126 // The 'target' might get missing if extensions do removeElement() in a form hook.
2127 if ($this->elementExists($target)) {
2128 $targetField = $this->getElement($target);
2129 $targetType = $targetField->getAttribute('data-callback') == 'civicrm/ajax/jqCounty' ? 'county' : 'stateProvince';
2130 $options = array();
2131 // If the control field is on the form, setup chain-select and dynamically populate options
2132 if ($this->elementExists($control)) {
2133 $controlField = $this->getElement($control);
2134 $controlType = $targetType == 'county' ? 'stateProvince' : 'country';
2135
2136 $targetField->setAttribute('class', $targetField->getAttribute('class') . ' crm-chain-select-target');
2137
2138 $css = (string) $controlField->getAttribute('class');
2139 $controlField->updateAttributes(array(
2140 'class' => ($css ? "$css " : 'crm-select2 ') . 'crm-chain-select-control',
2141 'data-target' => $target,
2142 ));
2143 $controlValue = $controlField->getValue();
2144 if ($controlValue) {
2145 $options = CRM_Core_BAO_Location::getChainSelectValues($controlValue, $controlType, TRUE);
2146 if (!$options) {
2147 $targetField->setAttribute('placeholder', $targetField->getAttribute('data-none-prompt'));
2148 }
2149 }
2150 else {
2151 $targetField->setAttribute('placeholder', $targetField->getAttribute('data-empty-prompt'));
2152 $targetField->setAttribute('disabled', 'disabled');
2153 }
2154 }
2155 // Control field not present - fall back to loading default options
2156 else {
2157 $options = CRM_Core_PseudoConstant::$targetType();
2158 }
2159 if (!$targetField->getAttribute('multiple')) {
2160 $options = array('' => $targetField->getAttribute('placeholder')) + $options;
2161 $targetField->removeAttribute('placeholder');
2162 }
2163 $targetField->_options = array();
2164 $targetField->loadArray($options);
2165 }
2166 }
2167 }
2168
2169 /**
2170 * Validate country / state / county match and suppress unwanted "required" errors
2171 */
2172 private function validateChainSelectFields() {
2173 foreach ($this->_chainSelectFields as $control => $target) {
2174 if ($this->elementExists($control) && $this->elementExists($target)) {
2175 $controlValue = (array) $this->getElementValue($control);
2176 $targetField = $this->getElement($target);
2177 $controlType = $targetField->getAttribute('data-callback') == 'civicrm/ajax/jqCounty' ? 'stateProvince' : 'country';
2178 $targetValue = array_filter((array) $targetField->getValue());
2179 if ($targetValue || $this->getElementError($target)) {
2180 $options = CRM_Core_BAO_Location::getChainSelectValues($controlValue, $controlType, TRUE);
2181 if ($targetValue) {
2182 if (!array_intersect($targetValue, array_keys($options))) {
2183 $this->setElementError($target, $controlType == 'country' ? ts('State/Province does not match the selected Country') : ts('County does not match the selected State/Province'));
2184 }
2185 } // Suppress "required" error for field if it has no options
2186 elseif (!$options) {
2187 $this->setElementError($target, NULL);
2188 }
2189 }
2190 }
2191 }
2192 }
2193
2194 }