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