Merge remote-tracking branch 'upstream/4.6' into 4.6-master-2015-07-27-10-02-15
[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 + array('check_permissions' => 1));
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
1367 case 'hidden':
1368 return $this->add('hidden', $name, NULL, $props, $required);
1369
1370 case 'TextArea':
1371 //Set default columns and rows for textarea.
1372 $props['rows'] = isset($props['rows']) ? $props['rows'] : 4;
1373 $props['cols'] = isset($props['cols']) ? $props['cols'] : 60;
1374 return $this->addElement('textarea', $name, $label, $props, $required);
1375
1376 case 'Select Date':
1377 //TODO: add range support
1378 //TODO: Add date formats
1379 //TODO: Add javascript template for dates.
1380 return $this->addDate($name, $label, $required, $props);
1381
1382 case 'Radio':
1383 $separator = isset($props['separator']) ? $props['separator'] : NULL;
1384 unset($props['separator']);
1385 if (!isset($props['allowClear'])) {
1386 $props['allowClear'] = !$required;
1387 }
1388 return $this->addRadio($name, $label, $options, $props, $separator, $required);
1389
1390 case 'Select':
1391 if (empty($props['multiple'])) {
1392 $options = array('' => $props['placeholder']) + $options;
1393 }
1394 // TODO: Add and/or option for fields that store multiple values
1395 return $this->add('select', $name, $label, $options, $required, $props);
1396
1397 case 'CheckBoxGroup':
1398 return $this->addCheckBox($name, $label, array_flip($options), $required, $props);
1399
1400 case 'RadioGroup':
1401 return $this->addRadio($name, $label, $options, $props, NULL, $required);
1402
1403 //case 'AdvMulti-Select':
1404 case 'CheckBox':
1405 $text = isset($props['text']) ? $props['text'] : NULL;
1406 unset($props['text']);
1407 return $this->addElement('checkbox', $name, $label, $text, $props);
1408
1409 case 'File':
1410 // We should not build upload file in search mode.
1411 if (isset($props['context']) && $props['context'] == 'search') {
1412 return;
1413 }
1414 $file = $this->add('file', $name, $label, $props, $required);
1415 $this->addUploadElement($name);
1416 return $file;
1417
1418 //case 'RichTextEditor':
1419 //TODO: Add javascript template for wysiwyg.
1420 case 'Autocomplete-Select':
1421 case 'EntityRef':
1422 return $this->addEntityRef($name, $label, $props, $required);
1423
1424 // Check datatypes of fields
1425 // case 'Int':
1426 //case 'Float':
1427 //case 'Money':
1428 //case 'Link':
1429 //case read only fields
1430 default:
1431 throw new Exception("Unsupported html-element " . $widget);
1432 }
1433 }
1434
1435 /**
1436 * Add a widget for selecting/editing/creating/copying a profile form
1437 *
1438 * @param string $name
1439 * HTML form-element name.
1440 * @param string $label
1441 * Printable label.
1442 * @param string $allowCoreTypes
1443 * Only present a UFGroup if its group_type includes a subset of $allowCoreTypes; e.g. 'Individual', 'Activity'.
1444 * @param string $allowSubTypes
1445 * Only present a UFGroup if its group_type is compatible with $allowSubypes.
1446 * @param array $entities
1447 * @param bool $default
1448 * //CRM-15427.
1449 */
1450 public function addProfileSelector($name, $label, $allowCoreTypes, $allowSubTypes, $entities, $default = FALSE) {
1451 // Output widget
1452 // FIXME: Instead of adhoc serialization, use a single json_encode()
1453 CRM_UF_Page_ProfileEditor::registerProfileScripts();
1454 CRM_UF_Page_ProfileEditor::registerSchemas(CRM_Utils_Array::collect('entity_type', $entities));
1455 $this->add('text', $name, $label, array(
1456 'class' => 'crm-profile-selector',
1457 // Note: client treats ';;' as equivalent to \0, and ';;' works better in HTML
1458 'data-group-type' => CRM_Core_BAO_UFGroup::encodeGroupType($allowCoreTypes, $allowSubTypes, ';;'),
1459 'data-entities' => json_encode($entities),
1460 //CRM-15427
1461 'data-default' => $default,
1462 ));
1463 }
1464
1465 /**
1466 * @param int $id
1467 * @param $title
1468 * @param null $required
1469 * @param null $extra
1470 */
1471 public function addCountry($id, $title, $required = NULL, $extra = NULL) {
1472 $this->addElement('select', $id, $title,
1473 array(
1474 '' => ts('- select -'),
1475 ) + CRM_Core_PseudoConstant::country(), $extra
1476 );
1477 if ($required) {
1478 $this->addRule($id, ts('Please select %1', array(1 => $title)), 'required');
1479 }
1480 }
1481
1482 /**
1483 * @param string $name
1484 * @param $label
1485 * @param $options
1486 * @param $attributes
1487 * @param null $required
1488 * @param null $javascriptMethod
1489 */
1490 public function addSelectOther($name, $label, $options, $attributes, $required = NULL, $javascriptMethod = NULL) {
1491
1492 $this->addElement('select', $name . '_id', $label, $options, $javascriptMethod);
1493
1494 if ($required) {
1495 $this->addRule($name . '_id', ts('Please select %1', array(1 => $label)), 'required');
1496 }
1497 }
1498
1499 /**
1500 * @return null
1501 */
1502 public function getRootTitle() {
1503 return NULL;
1504 }
1505
1506 /**
1507 * @return string
1508 */
1509 public function getCompleteTitle() {
1510 return $this->getRootTitle() . $this->getTitle();
1511 }
1512
1513 /**
1514 * @return CRM_Core_Smarty
1515 */
1516 public static function &getTemplate() {
1517 return self::$_template;
1518 }
1519
1520 /**
1521 * @param $elementName
1522 */
1523 public function addUploadElement($elementName) {
1524 $uploadNames = $this->get('uploadNames');
1525 if (!$uploadNames) {
1526 $uploadNames = array();
1527 }
1528 if (is_array($elementName)) {
1529 foreach ($elementName as $name) {
1530 if (!in_array($name, $uploadNames)) {
1531 $uploadNames[] = $name;
1532 }
1533 }
1534 }
1535 else {
1536 if (!in_array($elementName, $uploadNames)) {
1537 $uploadNames[] = $elementName;
1538 }
1539 }
1540 $this->set('uploadNames', $uploadNames);
1541
1542 $config = CRM_Core_Config::singleton();
1543 if (!empty($uploadNames)) {
1544 $this->controller->addUploadAction($config->customFileUploadDir, $uploadNames);
1545 }
1546 }
1547
1548 /**
1549 * @return string
1550 */
1551 public function buttonType() {
1552 $uploadNames = $this->get('uploadNames');
1553 $buttonType = (is_array($uploadNames) && !empty($uploadNames)) ? 'upload' : 'next';
1554 $this->assign('buttonType', $buttonType);
1555 return $buttonType;
1556 }
1557
1558 /**
1559 * @param $name
1560 *
1561 * @return null
1562 */
1563 public function getVar($name) {
1564 return isset($this->$name) ? $this->$name : NULL;
1565 }
1566
1567 /**
1568 * @param $name
1569 * @param $value
1570 */
1571 public function setVar($name, $value) {
1572 $this->$name = $value;
1573 }
1574
1575 /**
1576 * Add date.
1577 *
1578 * @code
1579 * // if you need time
1580 * $attributes = array(
1581 * 'addTime' => true,
1582 * 'formatType' => 'relative' or 'birth' etc check advanced date settings
1583 * );
1584 * @endcode
1585 *
1586 * @param string $name
1587 * Name of the element.
1588 * @param string $label
1589 * Label of the element.
1590 * @param bool $required
1591 * True if required.
1592 * @param array $attributes
1593 * Key / value pair.
1594 */
1595 public function addDate($name, $label, $required = FALSE, $attributes = NULL) {
1596 if (!empty($attributes['formatType'])) {
1597 // get actual format
1598 $params = array('name' => $attributes['formatType']);
1599 $values = array();
1600
1601 // cache date information
1602 static $dateFormat;
1603 $key = "dateFormat_" . str_replace(' ', '_', $attributes['formatType']);
1604 if (empty($dateFormat[$key])) {
1605 CRM_Core_DAO::commonRetrieve('CRM_Core_DAO_PreferencesDate', $params, $values);
1606 $dateFormat[$key] = $values;
1607 }
1608 else {
1609 $values = $dateFormat[$key];
1610 }
1611
1612 if ($values['date_format']) {
1613 $attributes['format'] = $values['date_format'];
1614 }
1615
1616 if (!empty($values['time_format'])) {
1617 $attributes['timeFormat'] = $values['time_format'];
1618 }
1619 $attributes['startOffset'] = $values['start'];
1620 $attributes['endOffset'] = $values['end'];
1621 }
1622
1623 $config = CRM_Core_Config::singleton();
1624 if (empty($attributes['format'])) {
1625 $attributes['format'] = $config->dateInputFormat;
1626 }
1627
1628 if (!isset($attributes['startOffset'])) {
1629 $attributes['startOffset'] = 10;
1630 }
1631
1632 if (!isset($attributes['endOffset'])) {
1633 $attributes['endOffset'] = 10;
1634 }
1635
1636 $this->add('text', $name, $label, $attributes);
1637
1638 if (!empty($attributes['addTime']) || !empty($attributes['timeFormat'])) {
1639
1640 if (!isset($attributes['timeFormat'])) {
1641 $timeFormat = $config->timeInputFormat;
1642 }
1643 else {
1644 $timeFormat = $attributes['timeFormat'];
1645 }
1646
1647 // 1 - 12 hours and 2 - 24 hours, but for jquery widget it is 0 and 1 respectively
1648 if ($timeFormat) {
1649 $show24Hours = TRUE;
1650 if ($timeFormat == 1) {
1651 $show24Hours = FALSE;
1652 }
1653
1654 //CRM-6664 -we are having time element name
1655 //in either flat string or an array format.
1656 $elementName = $name . '_time';
1657 if (substr($name, -1) == ']') {
1658 $elementName = substr($name, 0, strlen($name) - 1) . '_time]';
1659 }
1660
1661 $this->add('text', $elementName, ts('Time'), array('timeFormat' => $show24Hours));
1662 }
1663 }
1664
1665 if ($required) {
1666 $this->addRule($name, ts('Please select %1', array(1 => $label)), 'required');
1667 if (!empty($attributes['addTime']) && !empty($attributes['addTimeRequired'])) {
1668 $this->addRule($elementName, ts('Please enter a time.'), 'required');
1669 }
1670 }
1671 }
1672
1673 /**
1674 * Function that will add date and time.
1675 */
1676 public function addDateTime($name, $label, $required = FALSE, $attributes = NULL) {
1677 $addTime = array('addTime' => TRUE);
1678 if (is_array($attributes)) {
1679 $attributes = array_merge($attributes, $addTime);
1680 }
1681 else {
1682 $attributes = $addTime;
1683 }
1684
1685 $this->addDate($name, $label, $required, $attributes);
1686 }
1687
1688 /**
1689 * Add a currency and money element to the form.
1690 */
1691 public function addMoney(
1692 $name,
1693 $label,
1694 $required = FALSE,
1695 $attributes = NULL,
1696 $addCurrency = TRUE,
1697 $currencyName = 'currency',
1698 $defaultCurrency = NULL,
1699 $freezeCurrency = FALSE
1700 ) {
1701 $element = $this->add('text', $name, $label, $attributes, $required);
1702 $this->addRule($name, ts('Please enter a valid amount.'), 'money');
1703
1704 if ($addCurrency) {
1705 $ele = $this->addCurrency($currencyName, NULL, TRUE, $defaultCurrency, $freezeCurrency);
1706 }
1707
1708 return $element;
1709 }
1710
1711 /**
1712 * Add currency element to the form.
1713 */
1714 public function addCurrency(
1715 $name = 'currency',
1716 $label = NULL,
1717 $required = TRUE,
1718 $defaultCurrency = NULL,
1719 $freezeCurrency = FALSE
1720 ) {
1721 $currencies = CRM_Core_OptionGroup::values('currencies_enabled');
1722 $options = array('class' => 'crm-select2 eight');
1723 if (!$required) {
1724 $currencies = array('' => '') + $currencies;
1725 $options['placeholder'] = ts('- none -');
1726 }
1727 $ele = $this->add('select', $name, $label, $currencies, $required, $options);
1728 if ($freezeCurrency) {
1729 $ele->freeze();
1730 }
1731 if (!$defaultCurrency) {
1732 $config = CRM_Core_Config::singleton();
1733 $defaultCurrency = $config->defaultCurrency;
1734 }
1735 $this->setDefaults(array($name => $defaultCurrency));
1736 }
1737
1738 /**
1739 * Create a single or multiple entity ref field.
1740 * @param string $name
1741 * @param string $label
1742 * @param array $props
1743 * Mix of html and widget properties, including:.
1744 * - select - params to give to select2 widget
1745 * - entity - defaults to contact
1746 * - create - can the user create a new entity on-the-fly?
1747 * Set to TRUE if entity is contact and you want the default profiles,
1748 * or pass in your own set of links. @see CRM_Core_BAO_UFGroup::getCreateLinks for format
1749 * note that permissions are checked automatically
1750 * - api - array of settings for the getlist api wrapper
1751 * note that it accepts a 'params' setting which will be passed to the underlying api
1752 * - placeholder - string
1753 * - multiple - bool
1754 * - class, etc. - other html properties
1755 * @param bool $required
1756 *
1757 * @return HTML_QuickForm_Element
1758 */
1759 public function addEntityRef($name, $label = '', $props = array(), $required = FALSE) {
1760 require_once "api/api.php";
1761 $config = CRM_Core_Config::singleton();
1762 // Default properties
1763 $props['api'] = CRM_Utils_Array::value('api', $props, array());
1764 $props['entity'] = _civicrm_api_get_entity_name_from_camel(CRM_Utils_Array::value('entity', $props, 'contact'));
1765 $props['class'] = ltrim(CRM_Utils_Array::value('class', $props, '') . ' crm-form-entityref');
1766
1767 if ($props['entity'] == 'contact' && isset($props['create']) && !(CRM_Core_Permission::check('edit all contacts') || CRM_Core_Permission::check('add contacts'))) {
1768 unset($props['create']);
1769 }
1770
1771 $props['placeholder'] = CRM_Utils_Array::value('placeholder', $props, $required ? ts('- select %1 -', array(1 => ts(str_replace('_', ' ', $props['entity'])))) : ts('- none -'));
1772
1773 $defaults = array();
1774 if (!empty($props['multiple'])) {
1775 $defaults['multiple'] = TRUE;
1776 }
1777 $props['select'] = CRM_Utils_Array::value('select', $props, array()) + $defaults;
1778
1779 $this->formatReferenceFieldAttributes($props);
1780 return $this->add('text', $name, $label, $props, $required);
1781 }
1782
1783 /**
1784 * @param $props
1785 */
1786 private function formatReferenceFieldAttributes(&$props) {
1787 $props['data-select-params'] = json_encode($props['select']);
1788 $props['data-api-params'] = $props['api'] ? json_encode($props['api']) : NULL;
1789 $props['data-api-entity'] = $props['entity'];
1790 if (!empty($props['create'])) {
1791 $props['data-create-links'] = json_encode($props['create']);
1792 }
1793 CRM_Utils_Array::remove($props, 'multiple', 'select', 'api', 'entity', 'create');
1794 }
1795
1796 /**
1797 * Convert all date fields within the params to mysql date ready for the
1798 * BAO layer. In this case fields are checked against the $_datefields defined for the form
1799 * and if time is defined it is incorporated
1800 *
1801 * @param array $params
1802 * Input params from the form.
1803 *
1804 * @todo it would probably be better to work on $this->_params than a passed array
1805 * @todo standardise the format which dates are passed to the BAO layer in & remove date
1806 * handling from BAO
1807 */
1808 public function convertDateFieldsToMySQL(&$params) {
1809 foreach ($this->_dateFields as $fieldName => $specs) {
1810 if (!empty($params[$fieldName])) {
1811 $params[$fieldName] = CRM_Utils_Date::isoToMysql(
1812 CRM_Utils_Date::processDate(
1813 $params[$fieldName],
1814 CRM_Utils_Array::value("{$fieldName}_time", $params), TRUE)
1815 );
1816 }
1817 else {
1818 if (isset($specs['default'])) {
1819 $params[$fieldName] = date('YmdHis', strtotime($specs['default']));
1820 }
1821 }
1822 }
1823 }
1824
1825 /**
1826 * @param $elementName
1827 */
1828 public function removeFileRequiredRules($elementName) {
1829 $this->_required = array_diff($this->_required, array($elementName));
1830 if (isset($this->_rules[$elementName])) {
1831 foreach ($this->_rules[$elementName] as $index => $ruleInfo) {
1832 if ($ruleInfo['type'] == 'uploadedfile') {
1833 unset($this->_rules[$elementName][$index]);
1834 }
1835 }
1836 if (empty($this->_rules[$elementName])) {
1837 unset($this->_rules[$elementName]);
1838 }
1839 }
1840 }
1841
1842 /**
1843 * Function that can be defined in Form to override or.
1844 * perform specific action on cancel action
1845 */
1846 public function cancelAction() {
1847 }
1848
1849 /**
1850 * Helper function to verify that required fields have been filled.
1851 * Typically called within the scope of a FormRule function
1852 */
1853 public static function validateMandatoryFields($fields, $values, &$errors) {
1854 foreach ($fields as $name => $fld) {
1855 if (!empty($fld['is_required']) && CRM_Utils_System::isNull(CRM_Utils_Array::value($name, $values))) {
1856 $errors[$name] = ts('%1 is a required field.', array(1 => $fld['title']));
1857 }
1858 }
1859 }
1860
1861 /**
1862 * Get contact if for a form object. Prioritise
1863 * - cid in URL if 0 (on behalf on someoneelse)
1864 * (@todo consider setting a variable if onbehalf for clarity of downstream 'if's
1865 * - logged in user id if it matches the one in the cid in the URL
1866 * - contact id validated from a checksum from a checksum
1867 * - cid from the url if the caller has ACL permission to view
1868 * - fallback is logged in user (or ? NULL if no logged in user) (@todo wouldn't 0 be more intuitive?)
1869 *
1870 * @return NULL|int
1871 */
1872 public function getContactID() {
1873 $tempID = CRM_Utils_Request::retrieve('cid', 'Positive', $this);
1874 if (isset($this->_params) && isset($this->_params['select_contact_id'])) {
1875 $tempID = $this->_params['select_contact_id'];
1876 }
1877 if (isset($this->_params, $this->_params[0]) && !empty($this->_params[0]['select_contact_id'])) {
1878 // event form stores as an indexed array, contribution form not so much...
1879 $tempID = $this->_params[0]['select_contact_id'];
1880 }
1881
1882 // force to ignore the authenticated user
1883 if ($tempID === '0' || $tempID === 0) {
1884 // we set the cid on the form so that this will be retained for the Confirm page
1885 // in the multi-page form & prevent us returning the $userID when this is called
1886 // from that page
1887 // we don't really need to set it when $tempID is set because the params have that stored
1888 $this->set('cid', 0);
1889 return (int) $tempID;
1890 }
1891
1892 $userID = $this->getLoggedInUserContactID();
1893
1894 if (!is_null($tempID) && $tempID === $userID) {
1895 return (int) $userID;
1896 }
1897
1898 //check if this is a checksum authentication
1899 $userChecksum = CRM_Utils_Request::retrieve('cs', 'String', $this);
1900 if ($userChecksum) {
1901 //check for anonymous user.
1902 $validUser = CRM_Contact_BAO_Contact_Utils::validChecksum($tempID, $userChecksum);
1903 if ($validUser) {
1904 return $tempID;
1905 }
1906 }
1907 // check if user has permission, CRM-12062
1908 elseif ($tempID && CRM_Contact_BAO_Contact_Permission::allow($tempID)) {
1909 return $tempID;
1910 }
1911
1912 return is_numeric($userID) ? $userID : NULL;
1913 }
1914
1915 /**
1916 * Get the contact id of the logged in user.
1917 */
1918 public function getLoggedInUserContactID() {
1919 // check if the user is logged in and has a contact ID
1920 $session = CRM_Core_Session::singleton();
1921 return $session->get('userID');
1922 }
1923
1924 /**
1925 * Add autoselector field -if user has permission to view contacts
1926 * If adding this to a form you also need to add to the tpl e.g
1927 *
1928 * {if !empty($selectable)}
1929 * <div class="crm-summary-row">
1930 * <div class="crm-label">{$form.select_contact.label}</div>
1931 * <div class="crm-content">
1932 * {$form.select_contact.html}
1933 * </div>
1934 * </div>
1935 * {/if}
1936 *
1937 * @param array $profiles
1938 * Ids of profiles that are on the form (to be autofilled).
1939 * @param array $autoCompleteField
1940 *
1941 * - name_field
1942 * - id_field
1943 * - url (for ajax lookup)
1944 *
1945 * @todo add data attributes so we can deal with multiple instances on a form
1946 */
1947 public function addAutoSelector($profiles = array(), $autoCompleteField = array()) {
1948 $autoCompleteField = array_merge(array(
1949 'id_field' => 'select_contact_id',
1950 'placeholder' => ts('Select someone else ...'),
1951 'show_hide' => TRUE,
1952 'api' => array('params' => array('contact_type' => 'Individual')),
1953 ), $autoCompleteField);
1954
1955 if ($this->canUseAjaxContactLookups()) {
1956 $this->assign('selectable', $autoCompleteField['id_field']);
1957 $this->addEntityRef($autoCompleteField['id_field'], NULL, array(
1958 'placeholder' => $autoCompleteField['placeholder'],
1959 'api' => $autoCompleteField['api'],
1960 ));
1961
1962 CRM_Core_Resources::singleton()->addScriptFile('civicrm', 'js/AlternateContactSelector.js', 1, 'html-header')
1963 ->addSetting(array(
1964 'form' => array('autocompletes' => $autoCompleteField),
1965 'ids' => array('profile' => $profiles),
1966 ));
1967 }
1968 }
1969
1970 /**
1971 */
1972 public function canUseAjaxContactLookups() {
1973 if (0 < (civicrm_api3('contact', 'getcount', array('check_permissions' => 1))) &&
1974 CRM_Core_Permission::check(array(array('access AJAX API', 'access CiviCRM')))
1975 ) {
1976 return TRUE;
1977 }
1978 }
1979
1980 /**
1981 * Add the options appropriate to cid = zero - ie. autocomplete
1982 *
1983 * @todo there is considerable code duplication between the contribution forms & event forms. It is apparent
1984 * that small pieces of duplication are not being refactored into separate functions because their only shared parent
1985 * is this form. Inserting a class FrontEndForm.php between the contribution & event & this class would allow functions like this
1986 * and a dozen other small ones to be refactored into a shared parent with the reduction of much code duplication
1987 *
1988 * @param $onlinePaymentProcessorEnabled
1989 */
1990 public function addCIDZeroOptions($onlinePaymentProcessorEnabled) {
1991 $this->assign('nocid', TRUE);
1992 $profiles = array();
1993 if ($this->_values['custom_pre_id']) {
1994 $profiles[] = $this->_values['custom_pre_id'];
1995 }
1996 if ($this->_values['custom_post_id']) {
1997 $profiles = array_merge($profiles, (array) $this->_values['custom_post_id']);
1998 }
1999 if ($onlinePaymentProcessorEnabled) {
2000 $profiles[] = 'billing';
2001 }
2002 if (!empty($this->_values)) {
2003 $this->addAutoSelector($profiles);
2004 }
2005 }
2006
2007 /**
2008 * Set default values on form for given contact (or no contact defaults)
2009 *
2010 * @param mixed $profile_id
2011 * (can be id, or profile name).
2012 * @param int $contactID
2013 *
2014 * @return array
2015 */
2016 public function getProfileDefaults($profile_id = 'Billing', $contactID = NULL) {
2017 try {
2018 $defaults = civicrm_api3('profile', 'getsingle', array(
2019 'profile_id' => (array) $profile_id,
2020 'contact_id' => $contactID,
2021 ));
2022 return $defaults;
2023 }
2024 catch (Exception $e) {
2025 // the try catch block gives us silent failure -not 100% sure this is a good idea
2026 // as silent failures are often worse than noisy ones
2027 return array();
2028 }
2029 }
2030
2031 /**
2032 * Sets form attribute.
2033 * @see CRM.loadForm
2034 */
2035 public function preventAjaxSubmit() {
2036 $this->setAttribute('data-no-ajax-submit', 'true');
2037 }
2038
2039 /**
2040 * Sets form attribute.
2041 * @see CRM.loadForm
2042 */
2043 public function allowAjaxSubmit() {
2044 $this->removeAttribute('data-no-ajax-submit');
2045 }
2046
2047 /**
2048 * Sets page title based on entity and action.
2049 * @param string $entityLabel
2050 */
2051 public function setPageTitle($entityLabel) {
2052 switch ($this->_action) {
2053 case CRM_Core_Action::ADD:
2054 CRM_Utils_System::setTitle(ts('New %1', array(1 => $entityLabel)));
2055 break;
2056
2057 case CRM_Core_Action::UPDATE:
2058 CRM_Utils_System::setTitle(ts('Edit %1', array(1 => $entityLabel)));
2059 break;
2060
2061 case CRM_Core_Action::VIEW:
2062 case CRM_Core_Action::PREVIEW:
2063 CRM_Utils_System::setTitle(ts('View %1', array(1 => $entityLabel)));
2064 break;
2065
2066 case CRM_Core_Action::DELETE:
2067 CRM_Utils_System::setTitle(ts('Delete %1', array(1 => $entityLabel)));
2068 break;
2069 }
2070 }
2071
2072 /**
2073 * Create a chain-select target field. All settings are optional; the defaults usually work.
2074 *
2075 * @param string $elementName
2076 * @param array $settings
2077 *
2078 * @return HTML_QuickForm_Element
2079 */
2080 public function addChainSelect($elementName, $settings = array()) {
2081 $props = $settings += array(
2082 'control_field' => str_replace(array('state_province', 'StateProvince', 'county', 'County'), array(
2083 'country',
2084 'Country',
2085 'state_province',
2086 'StateProvince',
2087 ), $elementName),
2088 'data-callback' => strpos($elementName, 'rovince') ? 'civicrm/ajax/jqState' : 'civicrm/ajax/jqCounty',
2089 'label' => strpos($elementName, 'rovince') ? ts('State/Province') : ts('County'),
2090 'data-empty-prompt' => strpos($elementName, 'rovince') ? ts('Choose country first') : ts('Choose state first'),
2091 'data-none-prompt' => ts('- N/A -'),
2092 'multiple' => FALSE,
2093 'required' => FALSE,
2094 'placeholder' => empty($settings['required']) ? ts('- none -') : ts('- select -'),
2095 );
2096 CRM_Utils_Array::remove($props, 'label', 'required', 'control_field');
2097 $props['class'] = (empty($props['class']) ? '' : "{$props['class']} ") . 'crm-select2';
2098 $props['data-select-prompt'] = $props['placeholder'];
2099 $props['data-name'] = $elementName;
2100
2101 $this->_chainSelectFields[$settings['control_field']] = $elementName;
2102
2103 // Passing NULL instead of an array of options
2104 // CRM-15225 - normally QF will reject any selected values that are not part of the field's options, but due to a
2105 // quirk in our patched version of HTML_QuickForm_select, this doesn't happen if the options are NULL
2106 // which seems a bit dirty but it allows our dynamically-popuplated select element to function as expected.
2107 return $this->add('select', $elementName, $settings['label'], NULL, $settings['required'], $props);
2108 }
2109
2110 /**
2111 * Set options and attributes for chain select fields based on the controlling field's value
2112 */
2113 private function preProcessChainSelectFields() {
2114 foreach ($this->_chainSelectFields as $control => $target) {
2115 // The 'target' might get missing if extensions do removeElement() in a form hook.
2116 if ($this->elementExists($target)) {
2117 $targetField = $this->getElement($target);
2118 $targetType = $targetField->getAttribute('data-callback') == 'civicrm/ajax/jqCounty' ? 'county' : 'stateProvince';
2119 $options = array();
2120 // If the control field is on the form, setup chain-select and dynamically populate options
2121 if ($this->elementExists($control)) {
2122 $controlField = $this->getElement($control);
2123 $controlType = $targetType == 'county' ? 'stateProvince' : 'country';
2124
2125 $targetField->setAttribute('class', $targetField->getAttribute('class') . ' crm-chain-select-target');
2126
2127 $css = (string) $controlField->getAttribute('class');
2128 $controlField->updateAttributes(array(
2129 'class' => ($css ? "$css " : 'crm-select2 ') . 'crm-chain-select-control',
2130 'data-target' => $target,
2131 ));
2132 $controlValue = $controlField->getValue();
2133 if ($controlValue) {
2134 $options = CRM_Core_BAO_Location::getChainSelectValues($controlValue, $controlType, TRUE);
2135 if (!$options) {
2136 $targetField->setAttribute('placeholder', $targetField->getAttribute('data-none-prompt'));
2137 }
2138 }
2139 else {
2140 $targetField->setAttribute('placeholder', $targetField->getAttribute('data-empty-prompt'));
2141 $targetField->setAttribute('disabled', 'disabled');
2142 }
2143 }
2144 // Control field not present - fall back to loading default options
2145 else {
2146 $options = CRM_Core_PseudoConstant::$targetType();
2147 }
2148 if (!$targetField->getAttribute('multiple')) {
2149 $options = array('' => $targetField->getAttribute('placeholder')) + $options;
2150 $targetField->removeAttribute('placeholder');
2151 }
2152 $targetField->_options = array();
2153 $targetField->loadArray($options);
2154 }
2155 }
2156 }
2157
2158 /**
2159 * Validate country / state / county match and suppress unwanted "required" errors
2160 */
2161 private function validateChainSelectFields() {
2162 foreach ($this->_chainSelectFields as $control => $target) {
2163 if ($this->elementExists($control) && $this->elementExists($target)) {
2164 $controlValue = (array) $this->getElementValue($control);
2165 $targetField = $this->getElement($target);
2166 $controlType = $targetField->getAttribute('data-callback') == 'civicrm/ajax/jqCounty' ? 'stateProvince' : 'country';
2167 $targetValue = array_filter((array) $targetField->getValue());
2168 if ($targetValue || $this->getElementError($target)) {
2169 $options = CRM_Core_BAO_Location::getChainSelectValues($controlValue, $controlType, TRUE);
2170 if ($targetValue) {
2171 if (!array_intersect($targetValue, array_keys($options))) {
2172 $this->setElementError($target, $controlType == 'country' ? ts('State/Province does not match the selected Country') : ts('County does not match the selected State/Province'));
2173 }
2174 } // Suppress "required" error for field if it has no options
2175 elseif (!$options) {
2176 $this->setElementError($target, NULL);
2177 }
2178 }
2179 }
2180 }
2181 }
2182
2183 }