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