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