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