Merge remote-tracking branch 'upstream/4.6' into 4.6-master-2015-06-22-22-17-50
[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 public function setTitle($title) {
625 $this->_title = $title;
626 }
627
628 /**
629 * Assign billing type id to bltID.
630 *
631 * @throws CRM_Core_Exception
632 */
633 public function assignBillingType() {
634 $this->_bltID = CRM_Core_BAO_Location::getBillingLocationId();
635 $this->set('bltID', $this->_bltID);
636 $this->assign('bltID', $this->_bltID);
637 }
638
639 /**
640 * Setter function for options.
641 *
642 * @param mixed $options
643 *
644 * @return void
645 */
646 public function setOptions($options) {
647 $this->_options = $options;
648 }
649
650 /**
651 * Getter function for link.
652 *
653 * @return string
654 */
655 public function getLink() {
656 $config = CRM_Core_Config::singleton();
657 return CRM_Utils_System::url($_GET[$config->userFrameworkURLVar],
658 '_qf_' . $this->_name . '_display=true'
659 );
660 }
661
662 /**
663 * Boolean function to determine if this is a one form page.
664 *
665 * @return bool
666 */
667 public function isSimpleForm() {
668 return $this->_state->getType() & (CRM_Core_State::START | CRM_Core_State::FINISH);
669 }
670
671 /**
672 * Getter function for Form Action.
673 *
674 * @return string
675 */
676 public function getFormAction() {
677 return $this->_attributes['action'];
678 }
679
680 /**
681 * Setter function for Form Action.
682 *
683 * @param string $action
684 *
685 * @return void
686 */
687 public function setFormAction($action) {
688 $this->_attributes['action'] = $action;
689 }
690
691 /**
692 * Render form and return contents.
693 *
694 * @return string
695 */
696 public function toSmarty() {
697 $this->preProcessChainSelectFields();
698 $renderer = $this->getRenderer();
699 $this->accept($renderer);
700 $content = $renderer->toArray();
701 $content['formName'] = $this->getName();
702 // CRM-15153
703 $content['formClass'] = CRM_Utils_System::getClassName($this);
704 return $content;
705 }
706
707 /**
708 * Getter function for renderer. If renderer is not set
709 * create one and initialize it
710 *
711 * @return object
712 */
713 public function &getRenderer() {
714 if (!isset($this->_renderer)) {
715 $this->_renderer = CRM_Core_Form_Renderer::singleton();
716 }
717 return $this->_renderer;
718 }
719
720 /**
721 * Use the form name to create the tpl file name.
722 *
723 * @return string
724 */
725 public function getTemplateFileName() {
726 $ext = CRM_Extension_System::singleton()->getMapper();
727 if ($ext->isExtensionClass(CRM_Utils_System::getClassName($this))) {
728 $filename = $ext->getTemplateName(CRM_Utils_System::getClassName($this));
729 $tplname = $ext->getTemplatePath(CRM_Utils_System::getClassName($this)) . DIRECTORY_SEPARATOR . $filename;
730 }
731 else {
732 $tplname = strtr(
733 CRM_Utils_System::getClassName($this),
734 array(
735 '_' => DIRECTORY_SEPARATOR,
736 '\\' => DIRECTORY_SEPARATOR,
737 )
738 ) . '.tpl';
739 }
740 return $tplname;
741 }
742
743 /**
744 * A wrapper for getTemplateFileName that includes calling the hook to
745 * prevent us from having to copy & paste the logic of calling the hook
746 */
747 public function getHookedTemplateFileName() {
748 $pageTemplateFile = $this->getTemplateFileName();
749 CRM_Utils_Hook::alterTemplateFile(get_class($this), $this, 'page', $pageTemplateFile);
750 return $pageTemplateFile;
751 }
752
753 /**
754 * Default extra tpl file basically just replaces .tpl with .extra.tpl
755 * i.e. we dont override
756 *
757 * @return string
758 */
759 public function overrideExtraTemplateFileName() {
760 return NULL;
761 }
762
763 /**
764 * Error reporting mechanism.
765 *
766 * @param string $message
767 * Error Message.
768 * @param int $code
769 * Error Code.
770 * @param CRM_Core_DAO $dao
771 * A data access object on which we perform a rollback if non - empty.
772 *
773 * @return void
774 */
775 public function error($message, $code = NULL, $dao = NULL) {
776 if ($dao) {
777 $dao->query('ROLLBACK');
778 }
779
780 $error = CRM_Core_Error::singleton();
781
782 $error->push($code, $message);
783 }
784
785 /**
786 * Store the variable with the value in the form scope.
787 *
788 * @param string $name
789 * Name of the variable.
790 * @param mixed $value
791 * Value of the variable.
792 *
793 * @return void
794 */
795 public function set($name, $value) {
796 $this->controller->set($name, $value);
797 }
798
799 /**
800 * Get the variable from the form scope.
801 *
802 * @param string $name
803 * Name of the variable
804 *
805 * @return mixed
806 */
807 public function get($name) {
808 return $this->controller->get($name);
809 }
810
811 /**
812 * Getter for action.
813 *
814 * @return int
815 */
816 public function getAction() {
817 return $this->_action;
818 }
819
820 /**
821 * Setter for action.
822 *
823 * @param int $action
824 * The mode we want to set the form.
825 *
826 * @return void
827 */
828 public function setAction($action) {
829 $this->_action = $action;
830 }
831
832 /**
833 * Assign value to name in template.
834 *
835 * @param string $var
836 * Name of variable.
837 * @param mixed $value
838 * Value of variable.
839 *
840 * @return void
841 */
842 public function assign($var, $value = NULL) {
843 self::$_template->assign($var, $value);
844 }
845
846 /**
847 * Assign value to name in template by reference.
848 *
849 * @param string $var
850 * Name of variable.
851 * @param mixed $value
852 * Value of varaible.
853 *
854 * @return void
855 */
856 public function assign_by_ref($var, &$value) {
857 self::$_template->assign_by_ref($var, $value);
858 }
859
860 /**
861 * Appends values to template variables.
862 *
863 * @param array|string $tpl_var the template variable name(s)
864 * @param mixed $value
865 * The value to append.
866 * @param bool $merge
867 */
868 public function append($tpl_var, $value = NULL, $merge = FALSE) {
869 self::$_template->append($tpl_var, $value, $merge);
870 }
871
872 /**
873 * Returns an array containing template variables.
874 *
875 * @param string $name
876 *
877 * @return array
878 */
879 public function get_template_vars($name = NULL) {
880 return self::$_template->get_template_vars($name);
881 }
882
883 /**
884 * @param string $name
885 * @param $title
886 * @param $values
887 * @param array $attributes
888 * @param null $separator
889 * @param bool $required
890 *
891 * @return HTML_QuickForm_group
892 */
893 public function &addRadio($name, $title, $values, $attributes = array(), $separator = NULL, $required = FALSE) {
894 $options = array();
895 $attributes = $attributes ? $attributes : array();
896 $allowClear = !empty($attributes['allowClear']);
897 unset($attributes['allowClear']);
898 $attributes['id_suffix'] = $name;
899 foreach ($values as $key => $var) {
900 $options[] = $this->createElement('radio', NULL, NULL, $var, $key, $attributes);
901 }
902 $group = $this->addGroup($options, $name, $title, $separator);
903 if ($required) {
904 $this->addRule($name, ts('%1 is a required field.', array(1 => $title)), 'required');
905 }
906 if ($allowClear) {
907 $group->setAttribute('allowClear', TRUE);
908 }
909 return $group;
910 }
911
912 /**
913 * @param int $id
914 * @param $title
915 * @param bool $allowClear
916 * @param null $required
917 * @param array $attributes
918 */
919 public function addYesNo($id, $title, $allowClear = FALSE, $required = NULL, $attributes = array()) {
920 $attributes += array('id_suffix' => $id);
921 $choice = array();
922 $choice[] = $this->createElement('radio', NULL, '11', ts('Yes'), '1', $attributes);
923 $choice[] = $this->createElement('radio', NULL, '11', ts('No'), '0', $attributes);
924
925 $group = $this->addGroup($choice, $id, $title);
926 if ($allowClear) {
927 $group->setAttribute('allowClear', TRUE);
928 }
929 if ($required) {
930 $this->addRule($id, ts('%1 is a required field.', array(1 => $title)), 'required');
931 }
932 }
933
934 /**
935 * @param int $id
936 * @param $title
937 * @param $values
938 * @param null $other
939 * @param null $attributes
940 * @param null $required
941 * @param null $javascriptMethod
942 * @param string $separator
943 * @param bool $flipValues
944 */
945 public function addCheckBox(
946 $id, $title, $values, $other = NULL,
947 $attributes = NULL, $required = NULL,
948 $javascriptMethod = NULL,
949 $separator = '<br />', $flipValues = FALSE
950 ) {
951 $options = array();
952
953 if ($javascriptMethod) {
954 foreach ($values as $key => $var) {
955 if (!$flipValues) {
956 $options[] = $this->createElement('checkbox', $var, NULL, $key, $javascriptMethod);
957 }
958 else {
959 $options[] = $this->createElement('checkbox', $key, NULL, $var, $javascriptMethod);
960 }
961 }
962 }
963 else {
964 foreach ($values as $key => $var) {
965 if (!$flipValues) {
966 $options[] = $this->createElement('checkbox', $var, NULL, $key);
967 }
968 else {
969 $options[] = $this->createElement('checkbox', $key, NULL, $var);
970 }
971 }
972 }
973
974 $this->addGroup($options, $id, $title, $separator);
975
976 if ($other) {
977 $this->addElement('text', $id . '_other', ts('Other'), $attributes[$id . '_other']);
978 }
979
980 if ($required) {
981 $this->addRule($id,
982 ts('%1 is a required field.', array(1 => $title)),
983 'required'
984 );
985 }
986 }
987
988 public function resetValues() {
989 $data = $this->controller->container();
990 $data['values'][$this->_name] = array();
991 }
992
993 /**
994 * Simple shell that derived classes can call to add buttons to
995 * the form with a customized title for the main Submit
996 *
997 * @param string $title
998 * Title of the main button.
999 * @param string $nextType
1000 * Button type for the form after processing.
1001 * @param string $backType
1002 * @param bool|string $submitOnce If true, add javascript to next button submit which prevents it from being clicked more than once
1003 *
1004 * @return void
1005 */
1006 public function addDefaultButtons($title, $nextType = 'next', $backType = 'back', $submitOnce = FALSE) {
1007 $buttons = array();
1008 if ($backType != NULL) {
1009 $buttons[] = array(
1010 'type' => $backType,
1011 'name' => ts('Previous'),
1012 );
1013 }
1014 if ($nextType != NULL) {
1015 $nextButton = array(
1016 'type' => $nextType,
1017 'name' => $title,
1018 'isDefault' => TRUE,
1019 );
1020 if ($submitOnce) {
1021 $nextButton['js'] = array('onclick' => "return submitOnce(this,'{$this->_name}','" . ts('Processing') . "');");
1022 }
1023 $buttons[] = $nextButton;
1024 }
1025 $this->addButtons($buttons);
1026 }
1027
1028 /**
1029 * @param string $name
1030 * @param string $from
1031 * @param string $to
1032 * @param string $label
1033 * @param string $dateFormat
1034 * @param bool $required
1035 * @param bool $displayTime
1036 */
1037 public function addDateRange($name, $from = '_from', $to = '_to', $label = 'From:', $dateFormat = 'searchDate', $required = FALSE, $displayTime = FALSE) {
1038 if ($displayTime) {
1039 $this->addDateTime($name . $from, $label, $required, array('formatType' => $dateFormat));
1040 $this->addDateTime($name . $to, ts('To:'), $required, array('formatType' => $dateFormat));
1041 }
1042 else {
1043 $this->addDate($name . $from, $label, $required, array('formatType' => $dateFormat));
1044 $this->addDate($name . $to, ts('To:'), $required, array('formatType' => $dateFormat));
1045 }
1046 }
1047
1048 /**
1049 * Based on form action, return a string representing the api action.
1050 * Used by addField method.
1051 *
1052 * Return string
1053 */
1054 private function getApiAction() {
1055 $action = $this->getAction();
1056 if ($action & (CRM_Core_Action::UPDATE + CRM_Core_Action::ADD)) {
1057 return 'create';
1058 }
1059 if ($action & (CRM_Core_Action::BROWSE)) {
1060 return 'get';
1061 }
1062 // If you get this exception try adding more cases above.
1063 throw new Exception("Cannot determine api action for " . __CLASS__);
1064 }
1065
1066 /**
1067 * Classes extending CRM_Core_Form should implement this method.
1068 * @throws Exception
1069 */
1070 public function getDefaultEntity() {
1071 throw new Exception("Cannot determine default entity. The form class should implement getDefaultEntity().");
1072 }
1073
1074 /**
1075 * Classes extending CRM_Core_Form should implement this method.
1076 *
1077 * TODO: Merge with CRM_Core_DAO::buildOptionsContext($context) and add validation.
1078 * @throws Exception
1079 */
1080 public function getDefaultContext() {
1081 throw new Exception("Cannot determine default context. The form class should implement getDefaultContext().");
1082 }
1083
1084 /**
1085 * Adds a select based on field metadata.
1086 * TODO: This could be even more generic and widget type (select in this case) could also be read from metadata
1087 * Perhaps a method like $form->bind($name) which would look up all metadata for named field
1088 * @param $name
1089 * Field name to go on the form.
1090 * @param array $props
1091 * Mix of html attributes and special properties, namely.
1092 * - entity (api entity name, can usually be inferred automatically from the form class)
1093 * - field (field name - only needed if different from name used on the form)
1094 * - option_url - path to edit this option list - usually retrieved automatically - set to NULL to disable link
1095 * - placeholder - set to NULL to disable
1096 * - multiple - bool
1097 * - context - @see CRM_Core_DAO::buildOptionsContext
1098 * @param bool $required
1099 * @throws CRM_Core_Exception
1100 * @return HTML_QuickForm_Element
1101 */
1102 public function addSelect($name, $props = array(), $required = FALSE) {
1103 if (!isset($props['entity'])) {
1104 $props['entity'] = $this->getDefaultEntity();
1105 }
1106 if (!isset($props['field'])) {
1107 $props['field'] = strrpos($name, '[') ? rtrim(substr($name, 1 + strrpos($name, '[')), ']') : $name;
1108 }
1109 // Fetch options from the api unless passed explicitly
1110 if (isset($props['options'])) {
1111 $options = $props['options'];
1112 }
1113 else {
1114 $info = civicrm_api3($props['entity'], 'getoptions', $props);
1115 $options = $info['values'];
1116 }
1117 if (!array_key_exists('placeholder', $props)) {
1118 $props['placeholder'] = $required ? ts('- select -') : CRM_Utils_Array::value('context', $props) == 'search' ? ts('- any -') : ts('- none -');
1119 }
1120 // Handle custom field
1121 if (strpos($name, 'custom_') === 0 && is_numeric($name[7])) {
1122 list(, $id) = explode('_', $name);
1123 $label = isset($props['label']) ? $props['label'] : CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomField', 'label', $id);
1124 $gid = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomField', 'option_group_id', $id);
1125 if (CRM_Utils_Array::value('context', $props) != 'search') {
1126 $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);
1127 }
1128 }
1129 // Core field
1130 else {
1131 $info = civicrm_api3($props['entity'], 'getfields');
1132 foreach ($info['values'] as $uniqueName => $fieldSpec) {
1133 if (
1134 $uniqueName === $props['field'] ||
1135 CRM_Utils_Array::value('name', $fieldSpec) === $props['field'] ||
1136 in_array($props['field'], CRM_Utils_Array::value('api.aliases', $fieldSpec, array()))
1137 ) {
1138 break;
1139 }
1140 }
1141 $label = isset($props['label']) ? $props['label'] : $fieldSpec['title'];
1142 if (CRM_Utils_Array::value('context', $props) != 'search') {
1143 $props['data-option-edit-path'] = array_key_exists('option_url', $props) ? $props['option_url'] : $props['data-option-edit-path'] = CRM_Core_PseudoConstant::getOptionEditUrl($fieldSpec);
1144 }
1145 }
1146 $props['class'] = (isset($props['class']) ? $props['class'] . ' ' : '') . "crm-select2";
1147 $props['data-api-entity'] = $props['entity'];
1148 $props['data-api-field'] = $props['field'];
1149 CRM_Utils_Array::remove($props, 'label', 'entity', 'field', 'option_url', 'options', 'context');
1150 return $this->add('select', $name, $label, $options, $required, $props);
1151 }
1152
1153 /**
1154 * Adds a field based on metadata.
1155 *
1156 * @param $name
1157 * Field name to go on the form.
1158 * @param array $props
1159 * Mix of html attributes and special properties, namely.
1160 * - entity (api entity name, can usually be inferred automatically from the form class)
1161 * - name (field name - only needed if different from name used on the form)
1162 * - option_url - path to edit this option list - usually retrieved automatically - set to NULL to disable link
1163 * - placeholder - set to NULL to disable
1164 * - multiple - bool
1165 * - context - @see CRM_Core_DAO::buildOptionsContext
1166 * @param bool $required
1167 * @throws \CiviCRM_API3_Exception
1168 * @throws \Exception
1169 */
1170 public function addField($name, $props = array(), $required = FALSE) {
1171 // TODO: Handle custom field
1172 if (strpos($name, 'custom_') === 0 && is_numeric($name[7])) {
1173 throw new Exception("Custom fields are not supported by the addField method. ");
1174 }
1175 // Resolve context.
1176 if (!isset($props['context'])) {
1177 $props['context'] = $this->getDefaultContext();
1178 }
1179 // Resolve entity.
1180 if (!isset($props['entity'])) {
1181 $props['entity'] = $this->getDefaultEntity();
1182 }
1183 // Resolve field.
1184 if (!isset($props['name'])) {
1185 $props['name'] = strrpos($name, '[') ? rtrim(substr($name, 1 + strrpos($name, '[')), ']') : $name;
1186 }
1187 // Resolve action.
1188 if (!isset($props['action'])) {
1189 $props['action'] = $this->getApiAction();
1190 }
1191 // Get field metadata.
1192 $fieldSpec = civicrm_api3($props['entity'], 'getfield', $props);
1193 $fieldSpec = $fieldSpec['values'];
1194 $label = CRM_Utils_Array::value('label', $props, isset($fieldSpec['title']) ? $fieldSpec['title'] : NULL);
1195
1196 $widget = isset($props['type']) ? $props['type'] : $fieldSpec['html']['type'];
1197 if ($widget == 'TextArea' && $props['context'] == 'search') {
1198 $widget = 'Text';
1199 }
1200
1201 $isSelect = (in_array($widget, array(
1202 'Select',
1203 'Multi-Select',
1204 'Select State/Province',
1205 'Multi-Select State/Province',
1206 'Select Country',
1207 'Multi-Select Country',
1208 'AdvMulti-Select',
1209 'CheckBoxGroup',
1210 'RadioGroup',
1211 'Radio',
1212 )));
1213
1214 if ($isSelect) {
1215 // Fetch options from the api unless passed explicitly.
1216 if (isset($props['options'])) {
1217 $options = $props['options'];
1218 // Else this get passed to the form->add method.
1219 unset($props['options']);
1220 }
1221 else {
1222 $options = isset($fieldSpec['options']) ? $fieldSpec['options'] : NULL;
1223 }
1224 //@TODO AdvMulti-Select is deprecated, drop support.
1225 if ($props['context'] == 'search' || ($widget !== 'AdvMulti-Select' && strpos($widget, 'Select') !== FALSE)) {
1226 $widget = 'Select';
1227 }
1228 // Set default options-url value.
1229 if ((!isset($props['options-url']))) {
1230 $props['options-url'] = TRUE;
1231 }
1232
1233 // Add data for popup link.
1234 if ((isset($props['options-url']) && $props['options-url']) && ($props['context'] != 'search' && $widget == 'Select' && CRM_Core_Permission::check('administer CiviCRM'))) {
1235 $props['data-option-edit-path'] = array_key_exists('option_url', $props) ? $props['option_url'] : $props['data-option-edit-path'] = CRM_Core_PseudoConstant::getOptionEditUrl($fieldSpec);
1236 $props['data-api-entity'] = $props['entity'];
1237 $props['data-api-field'] = $props['name'];
1238 if (isset($props['options-url'])) {
1239 unset($props['options-url']);
1240 }
1241 }
1242 }
1243 //Use select2 library for following widgets.
1244 $isSelect2 = (in_array($widget, array(
1245 'Select',
1246 'Multi-Select',
1247 'Select State/Province',
1248 'Multi-Select State/Province',
1249 'Select Country',
1250 'Multi-Select Country',
1251 )));
1252 if ($isSelect2) {
1253 $props['class'] = (isset($props['class']) ? $props['class'] . ' ' : '') . "crm-select2";
1254 if ($props['context'] == 'search' || strpos($widget, 'Multi') !== FALSE) {
1255 $props['class'] .= ' huge';
1256 $props['multiple'] = 'multiple';
1257 }
1258 // The placeholder is only used for select-elements.
1259 if (!array_key_exists('placeholder', $props)) {
1260 $props['placeholder'] = $required ? ts('- select -') : $props['context'] == 'search' ? ts('- any -') : ts('- none -');
1261 }
1262 }
1263 $props += CRM_Utils_Array::value('html', $fieldSpec, array());
1264 CRM_Utils_Array::remove($props, 'entity', 'name', 'context', 'label', 'action', 'type');
1265 // TODO: refactor switch statement, to separate methods.
1266 switch ($widget) {
1267 case 'Text':
1268 case 'Link':
1269 //TODO: Autodetect ranges
1270 $props['size'] = isset($props['size']) ? $props['size'] : 60;
1271 $this->add('text', $name, $label, $props, $required);
1272 break;
1273
1274 case 'hidden':
1275 $this->add('hidden', $name, NULL, $props, $required);
1276 break;
1277
1278 case 'TextArea':
1279 //Set default columns and rows for textarea.
1280 $props['rows'] = isset($props['rows']) ? $props['rows'] : 4;
1281 $props['cols'] = isset($props['cols']) ? $props['cols'] : 60;
1282 $this->addElement('textarea', $name, $label, $props, $required);
1283 break;
1284
1285 case 'Select Date':
1286 //TODO: add range support
1287 //TODO: Add date formats
1288 //TODO: Add javascript template for dates.
1289 $this->addDate($name, $label, $required, $props);
1290 break;
1291
1292 case 'Radio':
1293 $separator = isset($props['separator']) ? $props['separator'] : NULL;
1294 unset($props['separator']);
1295 if (!isset($props['allowClear'])) {
1296 $props['allowClear'] = !$required;
1297 }
1298 $this->addRadio($name, $label, $options, $props, $separator, $required);
1299 break;
1300
1301 case 'Select':
1302 if (empty($props['multiple'])) {
1303 $options = array('' => $props['placeholder']) + $options;
1304 }
1305 $this->add('select', $name, $label, $options, $required, $props);
1306 // TODO: Add and/or option for fields that store multiple values
1307 break;
1308
1309 case 'CheckBoxGroup':
1310 $this->addCheckBox($name, $label, array_flip($options), $required, $props);
1311 break;
1312
1313 case 'RadioGroup':
1314 $this->addRadio($name, $label, $options, $props, NULL, $required);
1315 break;
1316
1317 //case 'AdvMulti-Select':
1318 case 'CheckBox':
1319 $text = isset($props['text']) ? $props['text'] : NULL;
1320 unset($props['text']);
1321 $this->addElement('checkbox', $name, $label, $text, $props);
1322 break;
1323
1324 case 'File':
1325 // We should not build upload file in search mode.
1326 if (isset($props['context']) && $props['context'] == 'search') {
1327 return;
1328 }
1329 $this->add('file', $name, $label, $props, $required);
1330 $this->addUploadElement($name);
1331 break;
1332
1333 //case 'RichTextEditor':
1334 //TODO: Add javascript template for wysiwyg.
1335 case 'Autocomplete-Select':
1336 case 'EntityRef':
1337 $this->addEntityRef($name, $label, $props, $required);
1338 break;
1339
1340 // Check datatypes of fields
1341 // case 'Int':
1342 //case 'Float':
1343 //case 'Money':
1344 //case 'Link':
1345 //case read only fields
1346 default:
1347 throw new Exception("Unsupported html-element " . $widget);
1348 }
1349 }
1350
1351 /**
1352 * Add a widget for selecting/editing/creating/copying a profile form
1353 *
1354 * @param string $name
1355 * HTML form-element name.
1356 * @param string $label
1357 * Printable label.
1358 * @param string $allowCoreTypes
1359 * Only present a UFGroup if its group_type includes a subset of $allowCoreTypes; e.g. 'Individual', 'Activity'.
1360 * @param string $allowSubTypes
1361 * Only present a UFGroup if its group_type is compatible with $allowSubypes.
1362 * @param array $entities
1363 * @param bool $default
1364 * //CRM-15427.
1365 */
1366 public function addProfileSelector($name, $label, $allowCoreTypes, $allowSubTypes, $entities, $default = FALSE) {
1367 // Output widget
1368 // FIXME: Instead of adhoc serialization, use a single json_encode()
1369 CRM_UF_Page_ProfileEditor::registerProfileScripts();
1370 CRM_UF_Page_ProfileEditor::registerSchemas(CRM_Utils_Array::collect('entity_type', $entities));
1371 $this->add('text', $name, $label, array(
1372 'class' => 'crm-profile-selector',
1373 // Note: client treats ';;' as equivalent to \0, and ';;' works better in HTML
1374 'data-group-type' => CRM_Core_BAO_UFGroup::encodeGroupType($allowCoreTypes, $allowSubTypes, ';;'),
1375 'data-entities' => json_encode($entities),
1376 //CRM-15427
1377 'data-default' => $default,
1378 ));
1379 }
1380
1381 /**
1382 * @param int $id
1383 * @param $title
1384 * @param null $required
1385 * @param null $extra
1386 */
1387 public function addCountry($id, $title, $required = NULL, $extra = NULL) {
1388 $this->addElement('select', $id, $title,
1389 array(
1390 '' => ts('- select -'),
1391 ) + CRM_Core_PseudoConstant::country(), $extra
1392 );
1393 if ($required) {
1394 $this->addRule($id, ts('Please select %1', array(1 => $title)), 'required');
1395 }
1396 }
1397
1398 /**
1399 * @param string $name
1400 * @param $label
1401 * @param $options
1402 * @param $attributes
1403 * @param null $required
1404 * @param null $javascriptMethod
1405 */
1406 public function addSelectOther($name, $label, $options, $attributes, $required = NULL, $javascriptMethod = NULL) {
1407
1408 $this->addElement('select', $name . '_id', $label, $options, $javascriptMethod);
1409
1410 if ($required) {
1411 $this->addRule($name . '_id', ts('Please select %1', array(1 => $label)), 'required');
1412 }
1413 }
1414
1415 /**
1416 * @return null
1417 */
1418 public function getRootTitle() {
1419 return NULL;
1420 }
1421
1422 /**
1423 * @return string
1424 */
1425 public function getCompleteTitle() {
1426 return $this->getRootTitle() . $this->getTitle();
1427 }
1428
1429 /**
1430 * @return CRM_Core_Smarty
1431 */
1432 public static function &getTemplate() {
1433 return self::$_template;
1434 }
1435
1436 /**
1437 * @param $elementName
1438 */
1439 public function addUploadElement($elementName) {
1440 $uploadNames = $this->get('uploadNames');
1441 if (!$uploadNames) {
1442 $uploadNames = array();
1443 }
1444 if (is_array($elementName)) {
1445 foreach ($elementName as $name) {
1446 if (!in_array($name, $uploadNames)) {
1447 $uploadNames[] = $name;
1448 }
1449 }
1450 }
1451 else {
1452 if (!in_array($elementName, $uploadNames)) {
1453 $uploadNames[] = $elementName;
1454 }
1455 }
1456 $this->set('uploadNames', $uploadNames);
1457
1458 $config = CRM_Core_Config::singleton();
1459 if (!empty($uploadNames)) {
1460 $this->controller->addUploadAction($config->customFileUploadDir, $uploadNames);
1461 }
1462 }
1463
1464 /**
1465 * @return string
1466 */
1467 public function buttonType() {
1468 $uploadNames = $this->get('uploadNames');
1469 $buttonType = (is_array($uploadNames) && !empty($uploadNames)) ? 'upload' : 'next';
1470 $this->assign('buttonType', $buttonType);
1471 return $buttonType;
1472 }
1473
1474 /**
1475 * @param $name
1476 *
1477 * @return null
1478 */
1479 public function getVar($name) {
1480 return isset($this->$name) ? $this->$name : NULL;
1481 }
1482
1483 /**
1484 * @param $name
1485 * @param $value
1486 */
1487 public function setVar($name, $value) {
1488 $this->$name = $value;
1489 }
1490
1491 /**
1492 * Add date.
1493 *
1494 * @code
1495 * // if you need time
1496 * $attributes = array(
1497 * 'addTime' => true,
1498 * 'formatType' => 'relative' or 'birth' etc check advanced date settings
1499 * );
1500 * @endcode
1501 *
1502 * @param string $name
1503 * Name of the element.
1504 * @param string $label
1505 * Label of the element.
1506 * @param bool $required
1507 * True if required.
1508 * @param array $attributes
1509 * Key / value pair.
1510 */
1511 public function addDate($name, $label, $required = FALSE, $attributes = NULL) {
1512 if (!empty($attributes['formatType'])) {
1513 // get actual format
1514 $params = array('name' => $attributes['formatType']);
1515 $values = array();
1516
1517 // cache date information
1518 static $dateFormat;
1519 $key = "dateFormat_" . str_replace(' ', '_', $attributes['formatType']);
1520 if (empty($dateFormat[$key])) {
1521 CRM_Core_DAO::commonRetrieve('CRM_Core_DAO_PreferencesDate', $params, $values);
1522 $dateFormat[$key] = $values;
1523 }
1524 else {
1525 $values = $dateFormat[$key];
1526 }
1527
1528 if ($values['date_format']) {
1529 $attributes['format'] = $values['date_format'];
1530 }
1531
1532 if (!empty($values['time_format'])) {
1533 $attributes['timeFormat'] = $values['time_format'];
1534 }
1535 $attributes['startOffset'] = $values['start'];
1536 $attributes['endOffset'] = $values['end'];
1537 }
1538
1539 $config = CRM_Core_Config::singleton();
1540 if (empty($attributes['format'])) {
1541 $attributes['format'] = $config->dateInputFormat;
1542 }
1543
1544 if (!isset($attributes['startOffset'])) {
1545 $attributes['startOffset'] = 10;
1546 }
1547
1548 if (!isset($attributes['endOffset'])) {
1549 $attributes['endOffset'] = 10;
1550 }
1551
1552 $this->add('text', $name, $label, $attributes);
1553
1554 if (!empty($attributes['addTime']) || !empty($attributes['timeFormat'])) {
1555
1556 if (!isset($attributes['timeFormat'])) {
1557 $timeFormat = $config->timeInputFormat;
1558 }
1559 else {
1560 $timeFormat = $attributes['timeFormat'];
1561 }
1562
1563 // 1 - 12 hours and 2 - 24 hours, but for jquery widget it is 0 and 1 respectively
1564 if ($timeFormat) {
1565 $show24Hours = TRUE;
1566 if ($timeFormat == 1) {
1567 $show24Hours = FALSE;
1568 }
1569
1570 //CRM-6664 -we are having time element name
1571 //in either flat string or an array format.
1572 $elementName = $name . '_time';
1573 if (substr($name, -1) == ']') {
1574 $elementName = substr($name, 0, strlen($name) - 1) . '_time]';
1575 }
1576
1577 $this->add('text', $elementName, ts('Time'), array('timeFormat' => $show24Hours));
1578 }
1579 }
1580
1581 if ($required) {
1582 $this->addRule($name, ts('Please select %1', array(1 => $label)), 'required');
1583 if (!empty($attributes['addTime']) && !empty($attributes['addTimeRequired'])) {
1584 $this->addRule($elementName, ts('Please enter a time.'), 'required');
1585 }
1586 }
1587 }
1588
1589 /**
1590 * Function that will add date and time.
1591 */
1592 public function addDateTime($name, $label, $required = FALSE, $attributes = NULL) {
1593 $addTime = array('addTime' => TRUE);
1594 if (is_array($attributes)) {
1595 $attributes = array_merge($attributes, $addTime);
1596 }
1597 else {
1598 $attributes = $addTime;
1599 }
1600
1601 $this->addDate($name, $label, $required, $attributes);
1602 }
1603
1604 /**
1605 * Add a currency and money element to the form.
1606 */
1607 public function addMoney(
1608 $name,
1609 $label,
1610 $required = FALSE,
1611 $attributes = NULL,
1612 $addCurrency = TRUE,
1613 $currencyName = 'currency',
1614 $defaultCurrency = NULL,
1615 $freezeCurrency = FALSE
1616 ) {
1617 $element = $this->add('text', $name, $label, $attributes, $required);
1618 $this->addRule($name, ts('Please enter a valid amount.'), 'money');
1619
1620 if ($addCurrency) {
1621 $ele = $this->addCurrency($currencyName, NULL, TRUE, $defaultCurrency, $freezeCurrency);
1622 }
1623
1624 return $element;
1625 }
1626
1627 /**
1628 * Add currency element to the form.
1629 */
1630 public function addCurrency(
1631 $name = 'currency',
1632 $label = NULL,
1633 $required = TRUE,
1634 $defaultCurrency = NULL,
1635 $freezeCurrency = FALSE
1636 ) {
1637 $currencies = CRM_Core_OptionGroup::values('currencies_enabled');
1638 $options = array('class' => 'crm-select2 eight');
1639 if (!$required) {
1640 $currencies = array('' => '') + $currencies;
1641 $options['placeholder'] = ts('- none -');
1642 }
1643 $ele = $this->add('select', $name, $label, $currencies, $required, $options);
1644 if ($freezeCurrency) {
1645 $ele->freeze();
1646 }
1647 if (!$defaultCurrency) {
1648 $config = CRM_Core_Config::singleton();
1649 $defaultCurrency = $config->defaultCurrency;
1650 }
1651 $this->setDefaults(array($name => $defaultCurrency));
1652 }
1653
1654 /**
1655 * Create a single or multiple entity ref field.
1656 * @param string $name
1657 * @param string $label
1658 * @param array $props
1659 * Mix of html and widget properties, including:.
1660 * - select - params to give to select2 widget
1661 * - entity - defaults to contact
1662 * - create - can the user create a new entity on-the-fly?
1663 * Set to TRUE if entity is contact and you want the default profiles,
1664 * or pass in your own set of links. @see CRM_Core_BAO_UFGroup::getCreateLinks for format
1665 * note that permissions are checked automatically
1666 * - api - array of settings for the getlist api wrapper
1667 * note that it accepts a 'params' setting which will be passed to the underlying api
1668 * - placeholder - string
1669 * - multiple - bool
1670 * - class, etc. - other html properties
1671 * @param bool $required
1672 *
1673 * @return HTML_QuickForm_Element
1674 */
1675 public function addEntityRef($name, $label = '', $props = array(), $required = FALSE) {
1676 require_once "api/api.php";
1677 $config = CRM_Core_Config::singleton();
1678 // Default properties
1679 $props['api'] = CRM_Utils_Array::value('api', $props, array());
1680 $props['entity'] = _civicrm_api_get_entity_name_from_camel(CRM_Utils_Array::value('entity', $props, 'contact'));
1681 $props['class'] = ltrim(CRM_Utils_Array::value('class', $props, '') . ' crm-form-entityref');
1682
1683 if ($props['entity'] == 'contact' && isset($props['create']) && !(CRM_Core_Permission::check('edit all contacts') || CRM_Core_Permission::check('add contacts'))) {
1684 unset($props['create']);
1685 }
1686
1687 $props['placeholder'] = CRM_Utils_Array::value('placeholder', $props, $required ? ts('- select %1 -', array(1 => ts(str_replace('_', ' ', $props['entity'])))) : ts('- none -'));
1688
1689 $defaults = array();
1690 if (!empty($props['multiple'])) {
1691 $defaults['multiple'] = TRUE;
1692 }
1693 $props['select'] = CRM_Utils_Array::value('select', $props, array()) + $defaults;
1694
1695 $this->formatReferenceFieldAttributes($props);
1696 return $this->add('text', $name, $label, $props, $required);
1697 }
1698
1699 /**
1700 * @param $props
1701 */
1702 private function formatReferenceFieldAttributes(&$props) {
1703 $props['data-select-params'] = json_encode($props['select']);
1704 $props['data-api-params'] = $props['api'] ? json_encode($props['api']) : NULL;
1705 $props['data-api-entity'] = $props['entity'];
1706 if (!empty($props['create'])) {
1707 $props['data-create-links'] = json_encode($props['create']);
1708 }
1709 CRM_Utils_Array::remove($props, 'multiple', 'select', 'api', 'entity', 'create');
1710 }
1711
1712 /**
1713 * Convert all date fields within the params to mysql date ready for the
1714 * BAO layer. In this case fields are checked against the $_datefields defined for the form
1715 * and if time is defined it is incorporated
1716 *
1717 * @param array $params
1718 * Input params from the form.
1719 *
1720 * @todo it would probably be better to work on $this->_params than a passed array
1721 * @todo standardise the format which dates are passed to the BAO layer in & remove date
1722 * handling from BAO
1723 */
1724 public function convertDateFieldsToMySQL(&$params) {
1725 foreach ($this->_dateFields as $fieldName => $specs) {
1726 if (!empty($params[$fieldName])) {
1727 $params[$fieldName] = CRM_Utils_Date::isoToMysql(
1728 CRM_Utils_Date::processDate(
1729 $params[$fieldName],
1730 CRM_Utils_Array::value("{$fieldName}_time", $params), TRUE)
1731 );
1732 }
1733 else {
1734 if (isset($specs['default'])) {
1735 $params[$fieldName] = date('YmdHis', strtotime($specs['default']));
1736 }
1737 }
1738 }
1739 }
1740
1741 /**
1742 * @param $elementName
1743 */
1744 public function removeFileRequiredRules($elementName) {
1745 $this->_required = array_diff($this->_required, array($elementName));
1746 if (isset($this->_rules[$elementName])) {
1747 foreach ($this->_rules[$elementName] as $index => $ruleInfo) {
1748 if ($ruleInfo['type'] == 'uploadedfile') {
1749 unset($this->_rules[$elementName][$index]);
1750 }
1751 }
1752 if (empty($this->_rules[$elementName])) {
1753 unset($this->_rules[$elementName]);
1754 }
1755 }
1756 }
1757
1758 /**
1759 * Function that can be defined in Form to override or.
1760 * perform specific action on cancel action
1761 */
1762 public function cancelAction() {
1763 }
1764
1765 /**
1766 * Helper function to verify that required fields have been filled.
1767 * Typically called within the scope of a FormRule function
1768 */
1769 public static function validateMandatoryFields($fields, $values, &$errors) {
1770 foreach ($fields as $name => $fld) {
1771 if (!empty($fld['is_required']) && CRM_Utils_System::isNull(CRM_Utils_Array::value($name, $values))) {
1772 $errors[$name] = ts('%1 is a required field.', array(1 => $fld['title']));
1773 }
1774 }
1775 }
1776
1777 /**
1778 * Get contact if for a form object. Prioritise
1779 * - cid in URL if 0 (on behalf on someoneelse)
1780 * (@todo consider setting a variable if onbehalf for clarity of downstream 'if's
1781 * - logged in user id if it matches the one in the cid in the URL
1782 * - contact id validated from a checksum from a checksum
1783 * - cid from the url if the caller has ACL permission to view
1784 * - fallback is logged in user (or ? NULL if no logged in user) (@todo wouldn't 0 be more intuitive?)
1785 *
1786 * @return NULL|int
1787 */
1788 public function getContactID() {
1789 $tempID = CRM_Utils_Request::retrieve('cid', 'Positive', $this);
1790 if (isset($this->_params) && isset($this->_params['select_contact_id'])) {
1791 $tempID = $this->_params['select_contact_id'];
1792 }
1793 if (isset($this->_params, $this->_params[0]) && !empty($this->_params[0]['select_contact_id'])) {
1794 // event form stores as an indexed array, contribution form not so much...
1795 $tempID = $this->_params[0]['select_contact_id'];
1796 }
1797
1798 // force to ignore the authenticated user
1799 if ($tempID === '0' || $tempID === 0) {
1800 // we set the cid on the form so that this will be retained for the Confirm page
1801 // in the multi-page form & prevent us returning the $userID when this is called
1802 // from that page
1803 // we don't really need to set it when $tempID is set because the params have that stored
1804 $this->set('cid', 0);
1805 return (int) $tempID;
1806 }
1807
1808 $userID = $this->getLoggedInUserContactID();
1809
1810 if (!is_null($tempID) && $tempID === $userID) {
1811 return (int) $userID;
1812 }
1813
1814 //check if this is a checksum authentication
1815 $userChecksum = CRM_Utils_Request::retrieve('cs', 'String', $this);
1816 if ($userChecksum) {
1817 //check for anonymous user.
1818 $validUser = CRM_Contact_BAO_Contact_Utils::validChecksum($tempID, $userChecksum);
1819 if ($validUser) {
1820 return $tempID;
1821 }
1822 }
1823 // check if user has permission, CRM-12062
1824 elseif ($tempID && CRM_Contact_BAO_Contact_Permission::allow($tempID)) {
1825 return $tempID;
1826 }
1827
1828 return is_numeric($userID) ? $userID : NULL;
1829 }
1830
1831 /**
1832 * Get the contact id of the logged in user.
1833 */
1834 public function getLoggedInUserContactID() {
1835 // check if the user is logged in and has a contact ID
1836 $session = CRM_Core_Session::singleton();
1837 return $session->get('userID');
1838 }
1839
1840 /**
1841 * Add autoselector field -if user has permission to view contacts
1842 * If adding this to a form you also need to add to the tpl e.g
1843 *
1844 * {if !empty($selectable)}
1845 * <div class="crm-summary-row">
1846 * <div class="crm-label">{$form.select_contact.label}</div>
1847 * <div class="crm-content">
1848 * {$form.select_contact.html}
1849 * </div>
1850 * </div>
1851 * {/if}
1852 *
1853 * @param array $profiles
1854 * Ids of profiles that are on the form (to be autofilled).
1855 * @param array $autoCompleteField
1856 *
1857 * - name_field
1858 * - id_field
1859 * - url (for ajax lookup)
1860 *
1861 * @todo add data attributes so we can deal with multiple instances on a form
1862 */
1863 public function addAutoSelector($profiles = array(), $autoCompleteField = array()) {
1864 $autoCompleteField = array_merge(array(
1865 'id_field' => 'select_contact_id',
1866 'placeholder' => ts('Select someone else ...'),
1867 'show_hide' => TRUE,
1868 'api' => array('params' => array('contact_type' => 'Individual')),
1869 ), $autoCompleteField);
1870
1871 if ($this->canUseAjaxContactLookups()) {
1872 $this->assign('selectable', $autoCompleteField['id_field']);
1873 $this->addEntityRef($autoCompleteField['id_field'], NULL, array(
1874 'placeholder' => $autoCompleteField['placeholder'],
1875 'api' => $autoCompleteField['api'],
1876 ));
1877
1878 CRM_Core_Resources::singleton()->addScriptFile('civicrm', 'js/AlternateContactSelector.js', 1, 'html-header')
1879 ->addSetting(array(
1880 'form' => array('autocompletes' => $autoCompleteField),
1881 'ids' => array('profile' => $profiles),
1882 ));
1883 }
1884 }
1885
1886 /**
1887 */
1888 public function canUseAjaxContactLookups() {
1889 if (0 < (civicrm_api3('contact', 'getcount', array('check_permissions' => 1))) &&
1890 CRM_Core_Permission::check(array(array('access AJAX API', 'access CiviCRM')))
1891 ) {
1892 return TRUE;
1893 }
1894 }
1895
1896 /**
1897 * Add the options appropriate to cid = zero - ie. autocomplete
1898 *
1899 * @todo there is considerable code duplication between the contribution forms & event forms. It is apparent
1900 * that small pieces of duplication are not being refactored into separate functions because their only shared parent
1901 * is this form. Inserting a class FrontEndForm.php between the contribution & event & this class would allow functions like this
1902 * and a dozen other small ones to be refactored into a shared parent with the reduction of much code duplication
1903 *
1904 * @param $onlinePaymentProcessorEnabled
1905 */
1906 public function addCIDZeroOptions($onlinePaymentProcessorEnabled) {
1907 $this->assign('nocid', TRUE);
1908 $profiles = array();
1909 if ($this->_values['custom_pre_id']) {
1910 $profiles[] = $this->_values['custom_pre_id'];
1911 }
1912 if ($this->_values['custom_post_id']) {
1913 $profiles = array_merge($profiles, (array) $this->_values['custom_post_id']);
1914 }
1915 if ($onlinePaymentProcessorEnabled) {
1916 $profiles[] = 'billing';
1917 }
1918 if (!empty($this->_values)) {
1919 $this->addAutoSelector($profiles);
1920 }
1921 }
1922
1923 /**
1924 * Set default values on form for given contact (or no contact defaults)
1925 *
1926 * @param mixed $profile_id
1927 * (can be id, or profile name).
1928 * @param int $contactID
1929 *
1930 * @return array
1931 */
1932 public function getProfileDefaults($profile_id = 'Billing', $contactID = NULL) {
1933 try {
1934 $defaults = civicrm_api3('profile', 'getsingle', array(
1935 'profile_id' => (array) $profile_id,
1936 'contact_id' => $contactID,
1937 ));
1938 return $defaults;
1939 }
1940 catch (Exception $e) {
1941 // the try catch block gives us silent failure -not 100% sure this is a good idea
1942 // as silent failures are often worse than noisy ones
1943 return array();
1944 }
1945 }
1946
1947 /**
1948 * Sets form attribute.
1949 * @see CRM.loadForm
1950 */
1951 public function preventAjaxSubmit() {
1952 $this->setAttribute('data-no-ajax-submit', 'true');
1953 }
1954
1955 /**
1956 * Sets form attribute.
1957 * @see CRM.loadForm
1958 */
1959 public function allowAjaxSubmit() {
1960 $this->removeAttribute('data-no-ajax-submit');
1961 }
1962
1963 /**
1964 * Sets page title based on entity and action.
1965 * @param string $entityLabel
1966 */
1967 public function setPageTitle($entityLabel) {
1968 switch ($this->_action) {
1969 case CRM_Core_Action::ADD:
1970 CRM_Utils_System::setTitle(ts('New %1', array(1 => $entityLabel)));
1971 break;
1972
1973 case CRM_Core_Action::UPDATE:
1974 CRM_Utils_System::setTitle(ts('Edit %1', array(1 => $entityLabel)));
1975 break;
1976
1977 case CRM_Core_Action::VIEW:
1978 case CRM_Core_Action::PREVIEW:
1979 CRM_Utils_System::setTitle(ts('View %1', array(1 => $entityLabel)));
1980 break;
1981
1982 case CRM_Core_Action::DELETE:
1983 CRM_Utils_System::setTitle(ts('Delete %1', array(1 => $entityLabel)));
1984 break;
1985 }
1986 }
1987
1988 /**
1989 * Create a chain-select target field. All settings are optional; the defaults usually work.
1990 *
1991 * @param string $elementName
1992 * @param array $settings
1993 *
1994 * @return HTML_QuickForm_Element
1995 */
1996 public function addChainSelect($elementName, $settings = array()) {
1997 $props = $settings += array(
1998 'control_field' => str_replace(array('state_province', 'StateProvince', 'county', 'County'), array(
1999 'country',
2000 'Country',
2001 'state_province',
2002 'StateProvince',
2003 ), $elementName),
2004 'data-callback' => strpos($elementName, 'rovince') ? 'civicrm/ajax/jqState' : 'civicrm/ajax/jqCounty',
2005 'label' => strpos($elementName, 'rovince') ? ts('State/Province') : ts('County'),
2006 'data-empty-prompt' => strpos($elementName, 'rovince') ? ts('Choose country first') : ts('Choose state first'),
2007 'data-none-prompt' => ts('- N/A -'),
2008 'multiple' => FALSE,
2009 'required' => FALSE,
2010 'placeholder' => empty($settings['required']) ? ts('- none -') : ts('- select -'),
2011 );
2012 CRM_Utils_Array::remove($props, 'label', 'required', 'control_field');
2013 $props['class'] = (empty($props['class']) ? '' : "{$props['class']} ") . 'crm-select2';
2014 $props['data-select-prompt'] = $props['placeholder'];
2015 $props['data-name'] = $elementName;
2016
2017 $this->_chainSelectFields[$settings['control_field']] = $elementName;
2018
2019 // Passing NULL instead of an array of options
2020 // CRM-15225 - normally QF will reject any selected values that are not part of the field's options, but due to a
2021 // quirk in our patched version of HTML_QuickForm_select, this doesn't happen if the options are NULL
2022 // which seems a bit dirty but it allows our dynamically-popuplated select element to function as expected.
2023 return $this->add('select', $elementName, $settings['label'], NULL, $settings['required'], $props);
2024 }
2025
2026 /**
2027 * Set options and attributes for chain select fields based on the controlling field's value
2028 */
2029 private function preProcessChainSelectFields() {
2030 foreach ($this->_chainSelectFields as $control => $target) {
2031 // The 'target' might get missing if extensions do removeElement() in a form hook.
2032 if ($this->elementExists($target)) {
2033 $targetField = $this->getElement($target);
2034 $targetType = $targetField->getAttribute('data-callback') == 'civicrm/ajax/jqCounty' ? 'county' : 'stateProvince';
2035 $options = array();
2036 // If the control field is on the form, setup chain-select and dynamically populate options
2037 if ($this->elementExists($control)) {
2038 $controlField = $this->getElement($control);
2039 $controlType = $targetType == 'county' ? 'stateProvince' : 'country';
2040
2041 $targetField->setAttribute('class', $targetField->getAttribute('class') . ' crm-chain-select-target');
2042
2043 $css = (string) $controlField->getAttribute('class');
2044 $controlField->updateAttributes(array(
2045 'class' => ($css ? "$css " : 'crm-select2 ') . 'crm-chain-select-control',
2046 'data-target' => $target,
2047 ));
2048 $controlValue = $controlField->getValue();
2049 if ($controlValue) {
2050 $options = CRM_Core_BAO_Location::getChainSelectValues($controlValue, $controlType, TRUE);
2051 if (!$options) {
2052 $targetField->setAttribute('placeholder', $targetField->getAttribute('data-none-prompt'));
2053 }
2054 }
2055 else {
2056 $targetField->setAttribute('placeholder', $targetField->getAttribute('data-empty-prompt'));
2057 $targetField->setAttribute('disabled', 'disabled');
2058 }
2059 }
2060 // Control field not present - fall back to loading default options
2061 else {
2062 $options = CRM_Core_PseudoConstant::$targetType();
2063 }
2064 if (!$targetField->getAttribute('multiple')) {
2065 $options = array('' => $targetField->getAttribute('placeholder')) + $options;
2066 $targetField->removeAttribute('placeholder');
2067 }
2068 $targetField->_options = array();
2069 $targetField->loadArray($options);
2070 }
2071 }
2072 }
2073
2074 /**
2075 * Validate country / state / county match and suppress unwanted "required" errors
2076 */
2077 private function validateChainSelectFields() {
2078 foreach ($this->_chainSelectFields as $control => $target) {
2079 if ($this->elementExists($control) && $this->elementExists($target)) {
2080 $controlValue = (array) $this->getElementValue($control);
2081 $targetField = $this->getElement($target);
2082 $controlType = $targetField->getAttribute('data-callback') == 'civicrm/ajax/jqCounty' ? 'stateProvince' : 'country';
2083 $targetValue = array_filter((array) $targetField->getValue());
2084 if ($targetValue || $this->getElementError($target)) {
2085 $options = CRM_Core_BAO_Location::getChainSelectValues($controlValue, $controlType, TRUE);
2086 if ($targetValue) {
2087 if (!array_intersect($targetValue, array_keys($options))) {
2088 $this->setElementError($target, $controlType == 'country' ? ts('State/Province does not match the selected Country') : ts('County does not match the selected State/Province'));
2089 }
2090 } // Suppress "required" error for field if it has no options
2091 elseif (!$options) {
2092 $this->setElementError($target, NULL);
2093 }
2094 }
2095 }
2096 }
2097 }
2098
2099 }