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