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