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