3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.5 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2014 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
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. |
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. |
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 +--------------------------------------------------------------------+
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
34 * @copyright CiviCRM LLC (c) 2004-2014
39 require_once 'HTML/QuickForm/Page.php';
44 class CRM_Core_Form
extends HTML_QuickForm_Page
{
47 * The state object that this form belongs to
53 * The name of this form
59 * The title of this form
62 protected $_title = NULL;
65 * The options passed into this form
68 protected $_options = NULL;
71 * The mode of operation for this form
77 * The renderer used for this form
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
89 * e.g on a form declare $_dateFields = array(
90 * 'receive_date' => array('default' => 'now'),
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
96 protected $_dateFields = array();
99 * Cache the smarty template for efficiency reasons
101 * @var CRM_Core_Smarty
103 static protected $_template;
106 * Indicate if this form should warn users of unsaved changes
108 protected $unsavedChangesWarn;
111 * What to return to the client if in ajax mode (snippet=json)
115 public $ajaxResponse = array();
118 * Url path used to reach this page
122 public $urlPath = array();
125 * @var CRM_Core_Controller
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
136 CONST ATTR_SPACING
= ' ';
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
146 CONST CB_PREFIX
= 'mark_x_', CB_PREFIY
= 'mark_y_', CB_PREFIZ
= 'mark_z_', CB_PREFIX_LEN
= 7;
149 * @internal to keep track of chain-select fields
152 private $_chainSelectFields = array();
155 * Constructor for the basic form page
157 * We should not use QuickForm directly. This class provides a lot
158 * of default convenient functions, rules and buttons
160 * @param object $state State associated with this form
161 * @param \const|\enum|int $action The mode the form is operating in (None/Create/View/Update/Delete)
162 * @param string $method The type of http method used (GET/POST)
163 * @param string $name The name of the form if different from class name
165 * @return \CRM_Core_Form
168 function __construct(
170 $action = CRM_Core_Action
::NONE
,
176 $this->_name
= $name;
179 // CRM-15153 - FIXME this name translates to a DOM id and is not always unique!
180 $this->_name
= CRM_Utils_String
::getClassName(CRM_Utils_System
::getClassName($this));
183 $this->HTML_QuickForm_Page($this->_name
, $method);
185 $this->_state
=& $state;
187 $this->_state
->setName($this->_name
);
189 $this->_action
= (int) $action;
191 $this->registerRules();
193 // let the constructor initialize this, should happen only once
194 if (!isset(self
::$_template)) {
195 self
::$_template = CRM_Core_Smarty
::singleton();
197 // Workaround for CRM-15153 - give each form a reasonably unique css class
198 $this->addClass(CRM_Utils_System
::getClassName($this));
200 $this->assign('snippet', CRM_Utils_Array
::value('snippet', $_GET));
203 static function generateID() {
207 * Add one or more css classes to the form
208 * @param string $className
210 public function addClass($className) {
211 $classes = $this->getAttribute('class');
212 $this->setAttribute('class', ($classes ?
"$classes " : '') . $className);
216 * Register all the standard rules that most forms potentially use
222 function registerRules() {
223 static $rules = array(
224 'title', 'longTitle', 'variable', 'qfVariable',
225 'phone', 'integer', 'query',
227 'domain', 'numberOfDigit',
228 'date', 'currentDate',
229 'asciiFile', 'htmlFile', 'utf8File',
230 'objectExists', 'optionExists', 'postalCode', 'money', 'positiveInteger',
231 'xssString', 'fileExists', 'autocomplete', 'validContact',
234 foreach ($rules as $rule) {
235 $this->registerRule($rule, 'callback', $rule, 'CRM_Utils_Rule');
240 * Simple easy to use wrapper around addElement. Deal with
241 * simple validation rules
243 * @param string $type
244 * @param string $name
245 * @param string $label
246 * @param string|array $attributes (options for select elements)
247 * @param bool $required
248 * @param array $extra (attributes for select elements)
250 * @return HTML_QuickForm_Element could be an error object
253 function &add($type, $name, $label = '',
254 $attributes = '', $required = FALSE, $extra = NULL
256 if ($type == 'select' && is_array($extra)) {
257 // Normalize this property
258 if (!empty($extra['multiple'])) {
259 $extra['multiple'] = 'multiple';
262 unset($extra['multiple']);
264 // Add placeholder option for select
265 if (isset($extra['placeholder'])) {
266 if ($extra['placeholder'] === TRUE) {
267 $extra['placeholder'] = $required ?
ts('- select -') : ts('- none -');
269 if (($extra['placeholder'] ||
$extra['placeholder'] === '') && empty($extra['multiple']) && is_array($attributes) && !isset($attributes[''])) {
270 $attributes = array('' => $extra['placeholder']) +
$attributes;
274 $element = $this->addElement($type, $name, $label, $attributes, $extra);
275 if (HTML_QuickForm
::isError($element)) {
276 CRM_Core_Error
::fatal(HTML_QuickForm
::errorMessage($element));
280 if ($type == 'file') {
281 $error = $this->addRule($name, ts('%1 is a required field.', array(1 => $label)), 'uploadedfile');
284 $error = $this->addRule($name, ts('%1 is a required field.', array(1 => $label)), 'required');
286 if (HTML_QuickForm
::isError($error)) {
287 CRM_Core_Error
::fatal(HTML_QuickForm
::errorMessage($element));
295 * This function is called before buildForm. Any pre-processing that
296 * needs to be done for buildForm should be done here
298 * This is a virtual function and should be redefined if needed
305 function preProcess() {}
308 * This function is called after the form is validated. Any
309 * processing of form state etc should be done in this function.
310 * Typically all processing associated with a form should be done
311 * here and relevant state should be stored in the session
313 * This is a virtual function and should be redefined if needed
320 function postProcess() {}
323 * This function is just a wrapper, so that we can call all the hook functions
324 * @param bool $allowAjax - FIXME: This feels kind of hackish, ideally we would take the json-related code from this function
325 * and bury it deeper down in the controller
327 function mainProcess($allowAjax = TRUE) {
328 $this->postProcess();
329 $this->postProcessHook();
331 // Respond with JSON if in AJAX context (also support legacy value '6')
332 if ($allowAjax && !empty($_REQUEST['snippet']) && in_array($_REQUEST['snippet'], array(CRM_Core_Smarty
::PRINT_JSON
, 6))) {
333 $this->ajaxResponse
['buttonName'] = str_replace('_qf_' . $this->getAttribute('id') . '_', '', $this->controller
->getButtonName());
334 $this->ajaxResponse
['action'] = $this->_action
;
335 if (isset($this->_id
) ||
isset($this->id
)) {
336 $this->ajaxResponse
['id'] = isset($this->id
) ?
$this->id
: $this->_id
;
338 CRM_Core_Page_AJAX
::returnJsonResponse($this->ajaxResponse
);
343 * The postProcess hook is typically called by the framework
344 * However in a few cases, the form exits or redirects early in which
345 * case it needs to call this function so other modules can do the needful
346 * Calling this function directly should be avoided if possible. In general a
347 * better way is to do setUserContext so the framework does the redirect
350 function postProcessHook() {
351 CRM_Utils_Hook
::postProcess(get_class($this), $this);
355 * This virtual function is used to build the form. It replaces the
356 * buildForm associated with QuickForm_Page. This allows us to put
357 * preProcess in front of the actual form building routine
364 function buildQuickForm() {}
367 * This virtual function is used to set the default values of
368 * various form elements
372 * @return array reference to the array of default values
375 function setDefaultValues() {}
378 * This is a virtual function that adds group and global rules to
379 * the form. Keeping it distinct from the form to keep code small
380 * and localized in the form building code
387 function addRules() {}
390 * Performs the server side validation
393 * @return boolean true if no error found
394 * @throws HTML_QuickForm_Error
396 function validate() {
397 $error = parent
::validate();
399 $this->validateChainSelectFields();
401 $hookErrors = CRM_Utils_Hook
::validate(
403 $this->_submitValues
,
408 if (!is_array($hookErrors)) {
409 $hookErrors = array();
412 CRM_Utils_Hook
::validateForm(
414 $this->_submitValues
,
420 if (!empty($hookErrors)) {
421 $this->_errors +
= $hookErrors;
424 return (0 == count($this->_errors
));
428 * Core function that builds the form. We redefine this function
429 * here and expect all CRM forms to build their form in the function
433 function buildForm() {
434 $this->_formBuilt
= TRUE;
438 CRM_Utils_Hook
::preProcess(get_class($this), $this);
440 $this->assign('translatePermission', CRM_Core_Permission
::check('translate CiviCRM'));
443 $this->controller
->_key
&&
444 $this->controller
->_generateQFKey
446 $this->addElement('hidden', 'qfKey', $this->controller
->_key
);
447 $this->assign('qfKey', $this->controller
->_key
);
451 // _generateQFKey suppresses the qfKey generation on form snippets that
452 // are part of other forms, hence we use that to avoid adding entryURL
453 if ($this->controller
->_generateQFKey
&& $this->controller
->_entryURL
) {
454 $this->addElement('hidden', 'entryURL', $this->controller
->_entryURL
);
457 $this->buildQuickForm();
459 $defaults = $this->setDefaultValues();
460 unset($defaults['qfKey']);
462 if (!empty($defaults)) {
463 $this->setDefaults($defaults);
466 // call the form hook
467 // also call the hook function so any modules can set thier own custom defaults
468 // the user can do both the form and set default values with this hook
469 CRM_Utils_Hook
::buildForm(get_class($this), $this);
473 //Set html data-attribute to enable warning user of unsaved changes
474 if ($this->unsavedChangesWarn
=== true
475 ||
(!isset($this->unsavedChangesWarn
)
476 && ($this->_action
& CRM_Core_Action
::ADD ||
$this->_action
& CRM_Core_Action
::UPDATE
)
479 $this->setAttribute('data-warn-changes', 'true');
484 * Add default Next / Back buttons
486 * @param array array of associative arrays in the order in which the buttons should be
487 * displayed. The associate array has 3 fields: 'type', 'name' and 'isDefault'
488 * The base form class will define a bunch of static arrays for commonly used
496 function addButtons($params) {
499 foreach ($params as $button) {
500 $js = CRM_Utils_Array
::value('js', $button);
501 $isDefault = CRM_Utils_Array
::value('isDefault', $button, FALSE);
503 $attrs = array('class' => 'crm-form-submit default');
506 $attrs = array('class' => 'crm-form-submit');
510 $attrs = array_merge($js, $attrs);
513 if ($button['type'] === 'cancel') {
514 $attrs['class'] .= ' cancel';
517 if ($button['type'] === 'reset') {
518 $prevnext[] = $this->createElement($button['type'], 'reset', $button['name'], $attrs);
521 if (!empty($button['subName'])) {
522 $buttonName = $this->getButtonName($button['type'], $button['subName']);
525 $buttonName = $this->getButtonName($button['type']);
528 if (in_array($button['type'], array('next', 'upload', 'done')) && $button['name'] === ts('Save')) {
529 $attrs = array_merge($attrs, (array('accesskey' => 'S')));
531 $prevnext[] = $this->createElement('submit', $buttonName, $button['name'], $attrs);
533 if (!empty($button['isDefault'])) {
534 $this->setDefaultAction($button['type']);
537 // if button type is upload, set the enctype
538 if ($button['type'] == 'upload') {
539 $this->updateAttributes(array('enctype' => 'multipart/form-data'));
540 $this->setMaxFileSize();
543 // hack - addGroup uses an array to express variable spacing, read from the last element
544 $spacing[] = CRM_Utils_Array
::value('spacing', $button, self
::ATTR_SPACING
);
546 $this->addGroup($prevnext, 'buttons', '', $spacing, FALSE);
550 * Getter function for Name
560 * Getter function for State
565 function &getState() {
566 return $this->_state
;
570 * Getter function for StateType
575 function getStateType() {
576 return $this->_state
->getType();
580 * Getter function for title. Should be over-ridden by derived class
585 function getTitle() {
586 return $this->_title ?
$this->_title
: ts('ERROR: Title is not Set');
590 * Setter function for title.
592 * @param string $title the title of the form
597 function setTitle($title) {
598 $this->_title
= $title;
602 * Setter function for options
609 function setOptions($options) {
610 $this->_options
= $options;
614 * Getter function for link.
620 $config = CRM_Core_Config
::singleton();
621 return CRM_Utils_System
::url($_GET[$config->userFrameworkURLVar
],
622 '_qf_' . $this->_name
. '_display=true'
627 * Boolean function to determine if this is a one form page
632 function isSimpleForm() {
633 return $this->_state
->getType() & (CRM_Core_State
::START | CRM_Core_State
::FINISH
);
637 * Getter function for Form Action
642 function getFormAction() {
643 return $this->_attributes
['action'];
647 * Setter function for Form Action
654 function setFormAction($action) {
655 $this->_attributes
['action'] = $action;
659 * Render form and return contents
664 function toSmarty() {
665 $this->preProcessChainSelectFields();
666 $renderer = $this->getRenderer();
667 $this->accept($renderer);
668 $content = $renderer->toArray();
669 $content['formName'] = $this->getName();
671 $content['formClass'] = CRM_Utils_System
::getClassName($this);
676 * Getter function for renderer. If renderer is not set
677 * create one and initialize it
682 function &getRenderer() {
683 if (!isset($this->_renderer
)) {
684 $this->_renderer
= CRM_Core_Form_Renderer
::singleton();
686 return $this->_renderer
;
690 * Use the form name to create the tpl file name
695 function getTemplateFileName() {
696 $ext = CRM_Extension_System
::singleton()->getMapper();
697 if ($ext->isExtensionClass(CRM_Utils_System
::getClassName($this))) {
698 $filename = $ext->getTemplateName(CRM_Utils_System
::getClassName($this));
699 $tplname = $ext->getTemplatePath(CRM_Utils_System
::getClassName($this)) . DIRECTORY_SEPARATOR
. $filename;
702 $tplname = str_replace('_',
704 CRM_Utils_System
::getClassName($this)
711 * A wrapper for getTemplateFileName that includes calling the hook to
712 * prevent us from having to copy & paste the logic of calling the hook
714 function getHookedTemplateFileName() {
715 $pageTemplateFile = $this->getTemplateFileName();
716 CRM_Utils_Hook
::alterTemplateFile(get_class($this), $this, 'page', $pageTemplateFile);
717 return $pageTemplateFile;
721 * Default extra tpl file basically just replaces .tpl with .extra.tpl
722 * i.e. we dont override
727 function overrideExtraTemplateFileName() {
732 * Error reporting mechanism
734 * @param string $message Error Message
735 * @param int $code Error Code
736 * @param CRM_Core_DAO $dao A data access object on which we perform a rollback if non - empty
741 function error($message, $code = NULL, $dao = NULL) {
743 $dao->query('ROLLBACK');
746 $error = CRM_Core_Error
::singleton();
748 $error->push($code, $message);
752 * Store the variable with the value in the form scope
754 * @param string name : name of the variable
755 * @param mixed value : value of the variable
762 function set($name, $value) {
763 $this->controller
->set($name, $value);
767 * Get the variable from the form scope
769 * @param string name : name of the variable
776 function get($name) {
777 return $this->controller
->get($name);
786 function getAction() {
787 return $this->_action
;
793 * @param int $action the mode we want to set the form
798 function setAction($action) {
799 $this->_action
= $action;
803 * Assign value to name in template
805 * @param string $var name of variable
806 * @param mixed $value value of variable
811 function assign($var, $value = NULL) {
812 self
::$_template->assign($var, $value);
816 * Assign value to name in template by reference
818 * @param string $var name of variable
819 * @param mixed $value value of varaible
824 function assign_by_ref($var, &$value) {
825 self
::$_template->assign_by_ref($var, $value);
829 * Appends values to template variables
831 * @param array|string $tpl_var the template variable name(s)
832 * @param mixed $value the value to append
835 function append($tpl_var, $value=NULL, $merge=FALSE) {
836 self
::$_template->append($tpl_var, $value, $merge);
840 * Returns an array containing template variables
842 * @param string $name
846 function get_template_vars($name=null) {
847 return self
::$_template->get_template_vars($name);
851 * @param string $name
854 * @param array $attributes
855 * @param null $separator
856 * @param bool $required
858 * @return HTML_QuickForm_group
860 function &addRadio($name, $title, $values, $attributes = array(), $separator = NULL, $required = FALSE) {
862 $attributes = $attributes ?
$attributes : array();
863 $allowClear = !empty($attributes['allowClear']);
864 unset($attributes['allowClear']);
865 $attributes +
= array('id_suffix' => $name);
866 foreach ($values as $key => $var) {
867 $options[] = $this->createElement('radio', NULL, NULL, $var, $key, $attributes);
869 $group = $this->addGroup($options, $name, $title, $separator);
871 $this->addRule($name, ts('%1 is a required field.', array(1 => $title)), 'required');
874 $group->setAttribute('allowClear', TRUE);
882 * @param bool $allowClear
883 * @param null $required
884 * @param array $attributes
886 function addYesNo($id, $title, $allowClear = FALSE, $required = NULL, $attributes = array()) {
887 $attributes +
= array('id_suffix' => $id);
889 $choice[] = $this->createElement('radio', NULL, '11', ts('Yes'), '1', $attributes);
890 $choice[] = $this->createElement('radio', NULL, '11', ts('No'), '0', $attributes);
892 $group = $this->addGroup($choice, $id, $title);
894 $group->setAttribute('allowClear', TRUE);
897 $this->addRule($id, ts('%1 is a required field.', array(1 => $title)), 'required');
906 * @param null $attributes
907 * @param null $required
908 * @param null $javascriptMethod
909 * @param string $separator
910 * @param bool $flipValues
912 function addCheckBox($id, $title, $values, $other = NULL,
913 $attributes = NULL, $required = NULL,
914 $javascriptMethod = NULL,
915 $separator = '<br />', $flipValues = FALSE
919 if ($javascriptMethod) {
920 foreach ($values as $key => $var) {
922 $options[] = $this->createElement('checkbox', $var, NULL, $key, $javascriptMethod);
925 $options[] = $this->createElement('checkbox', $key, NULL, $var, $javascriptMethod);
930 foreach ($values as $key => $var) {
932 $options[] = $this->createElement('checkbox', $var, NULL, $key);
935 $options[] = $this->createElement('checkbox', $key, NULL, $var);
940 $this->addGroup($options, $id, $title, $separator);
943 $this->addElement('text', $id . '_other', ts('Other'), $attributes[$id . '_other']);
948 ts('%1 is a required field.', array(1 => $title)),
954 function resetValues() {
955 $data = $this->controller
->container();
956 $data['values'][$this->_name
] = array();
960 * Simple shell that derived classes can call to add buttons to
961 * the form with a customized title for the main Submit
963 * @param string $title title of the main button
964 * @param string $nextType button type for the form after processing
965 * @param string $backType
966 * @param bool|string $submitOnce If true, add javascript to next button submit which prevents it from being clicked more than once
971 function addDefaultButtons($title, $nextType = 'next', $backType = 'back', $submitOnce = FALSE) {
973 if ($backType != NULL) {
976 'name' => ts('Previous'),
979 if ($nextType != NULL) {
986 $nextButton['js'] = array('onclick' => "return submitOnce(this,'{$this->_name}','" . ts('Processing') . "');");
988 $buttons[] = $nextButton;
990 $this->addButtons($buttons);
994 * @param string $name
995 * @param string $from
997 * @param string $label
998 * @param string $dateFormat
999 * @param bool $required
1000 * @param bool $displayTime
1002 function addDateRange($name, $from = '_from', $to = '_to', $label = 'From:', $dateFormat = 'searchDate', $required = FALSE, $displayTime = FALSE) {
1004 $this->addDateTime($name . $from, $label, $required, array('formatType' => $dateFormat));
1005 $this->addDateTime($name . $to, ts('To:'), $required, array('formatType' => $dateFormat));
1007 $this->addDate($name . $from, $label, $required, array('formatType' => $dateFormat));
1008 $this->addDate($name . $to, ts('To:'), $required, array('formatType' => $dateFormat));
1013 * Adds a select based on field metadata
1014 * TODO: This could be even more generic and widget type (select in this case) could also be read from metadata
1015 * Perhaps a method like $form->bind($name) which would look up all metadata for named field
1016 * @param $name - field name to go on the form
1017 * @param array $props - mix of html attributes and special properties, namely
1018 * - entity (api entity name, can usually be inferred automatically from the form class)
1019 * - field (field name - only needed if different from name used on the form)
1020 * - option_url - path to edit this option list - usually retrieved automatically - set to NULL to disable link
1021 * - placeholder - set to NULL to disable
1023 * @param bool $required
1024 * @throws CRM_Core_Exception
1025 * @return HTML_QuickForm_Element
1027 function addSelect($name, $props = array(), $required = FALSE) {
1028 if (!isset($props['entity'])) {
1029 $props['entity'] = CRM_Utils_Api
::getEntityName($this);
1031 if (!isset($props['field'])) {
1032 $props['field'] = strrpos($name, '[') ?
rtrim(substr($name, 1 +
strrpos($name, '[')), ']') : $name;
1034 // Fetch options from the api unless passed explicitly
1035 if (isset($props['options'])) {
1036 $options = $props['options'];
1039 $info = civicrm_api3($props['entity'], 'getoptions', array('field' => $props['field']));
1040 $options = $info['values'];
1042 if (!array_key_exists('placeholder', $props)) {
1043 $props['placeholder'] = $required ?
ts('- select -') : ts('- none -');
1045 // Handle custom field
1046 if (strpos($name, 'custom_') === 0 && is_numeric($name[7])) {
1047 list(, $id) = explode('_', $name);
1048 $label = isset($props['label']) ?
$props['label'] : CRM_Core_DAO
::getFieldValue('CRM_Core_DAO_CustomField', 'label', $id);
1049 $gid = CRM_Core_DAO
::getFieldValue('CRM_Core_DAO_CustomField', 'option_group_id', $id);
1050 $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);
1054 $info = civicrm_api3($props['entity'], 'getfields');
1055 foreach($info['values'] as $uniqueName => $fieldSpec) {
1057 $uniqueName === $props['field'] ||
1058 CRM_Utils_Array
::value('name', $fieldSpec) === $props['field'] ||
1059 in_array($props['field'], CRM_Utils_Array
::value('api.aliases', $fieldSpec, array()))
1064 $label = isset($props['label']) ?
$props['label'] : $fieldSpec['title'];
1065 $props['data-option-edit-path'] = array_key_exists('option_url', $props) ?
$props['option_url'] : $props['data-option-edit-path'] = CRM_Core_PseudoConstant
::getOptionEditUrl($fieldSpec);
1067 $props['class'] = (isset($props['class']) ?
$props['class'] . ' ' : '') . "crm-select2";
1068 $props['data-api-entity'] = $props['entity'];
1069 $props['data-api-field'] = $props['field'];
1070 CRM_Utils_Array
::remove($props, 'label', 'entity', 'field', 'option_url', 'options');
1071 return $this->add('select', $name, $label, $options, $required, $props);
1075 * Add a widget for selecting/editing/creating/copying a profile form
1077 * @param string $name HTML form-element name
1078 * @param string $label Printable label
1079 * @param string $allowCoreTypes only present a UFGroup if its group_type includes a subset of $allowCoreTypes; e.g. 'Individual', 'Activity'
1080 * @param string $allowSubTypes only present a UFGroup if its group_type is compatible with $allowSubypes
1081 * @param array $entities
1082 * @param bool $default //CRM-15427
1084 function addProfileSelector($name, $label, $allowCoreTypes, $allowSubTypes, $entities, $default = FALSE) {
1086 // FIXME: Instead of adhoc serialization, use a single json_encode()
1087 CRM_UF_Page_ProfileEditor
::registerProfileScripts();
1088 CRM_UF_Page_ProfileEditor
::registerSchemas(CRM_Utils_Array
::collect('entity_type', $entities));
1089 $this->add('text', $name, $label, array(
1090 'class' => 'crm-profile-selector',
1091 // Note: client treats ';;' as equivalent to \0, and ';;' works better in HTML
1092 'data-group-type' => CRM_Core_BAO_UFGroup
::encodeGroupType($allowCoreTypes, $allowSubTypes, ';;'),
1093 'data-entities' => json_encode($entities),
1095 'data-default' => $default,
1100 * @param string $name
1102 * @param $attributes
1103 * @param bool $forceTextarea
1105 function addWysiwyg($name, $label, $attributes, $forceTextarea = FALSE) {
1106 // 1. Get configuration option for editor (tinymce, ckeditor, pure textarea)
1107 // 2. Based on the option, initialise proper editor
1108 $editorID = CRM_Core_BAO_Setting
::getItem(CRM_Core_BAO_Setting
::SYSTEM_PREFERENCES_NAME
,
1111 $editor = strtolower(CRM_Utils_Array
::value($editorID,
1112 CRM_Core_OptionGroup
::values('wysiwyg_editor')
1114 if (!$editor ||
$forceTextarea) {
1115 $editor = 'textarea';
1117 if ($editor == 'joomla default editor') {
1118 $editor = 'joomlaeditor';
1121 if ($editor == 'drupal default editor') {
1122 $editor = 'drupalwysiwyg';
1125 //lets add the editor as a attribute
1126 $attributes['editor'] = $editor;
1128 $this->addElement($editor, $name, $label, $attributes);
1129 $this->assign('editor', $editor);
1131 // include wysiwyg editor js files
1132 // FIXME: This code does not make any sense
1133 $includeWysiwygEditor = FALSE;
1134 $includeWysiwygEditor = $this->get('includeWysiwygEditor');
1135 if (!$includeWysiwygEditor) {
1136 $includeWysiwygEditor = TRUE;
1137 $this->set('includeWysiwygEditor', $includeWysiwygEditor);
1140 $this->assign('includeWysiwygEditor', $includeWysiwygEditor);
1146 * @param null $required
1147 * @param null $extra
1149 function addCountry($id, $title, $required = NULL, $extra = NULL) {
1150 $this->addElement('select', $id, $title,
1152 '' => ts('- select -')) + CRM_Core_PseudoConstant
::country(), $extra
1155 $this->addRule($id, ts('Please select %1', array(1 => $title)), 'required');
1160 * @param string $name
1163 * @param $attributes
1164 * @param null $required
1165 * @param null $javascriptMethod
1167 function addSelectOther($name, $label, $options, $attributes, $required = NULL, $javascriptMethod = NULL) {
1169 $this->addElement('select', $name . '_id', $label, $options, $javascriptMethod);
1172 $this->addRule($name . '_id', ts('Please select %1', array(1 => $label)), 'required');
1179 public function getRootTitle() {
1186 public function getCompleteTitle() {
1187 return $this->getRootTitle() . $this->getTitle();
1191 * @return CRM_Core_Smarty
1193 static function &getTemplate() {
1194 return self
::$_template;
1198 * @param $elementName
1200 function addUploadElement($elementName) {
1201 $uploadNames = $this->get('uploadNames');
1202 if (!$uploadNames) {
1203 $uploadNames = array();
1205 if (is_array($elementName)) {
1206 foreach ($elementName as $name) {
1207 if (!in_array($name, $uploadNames)) {
1208 $uploadNames[] = $name;
1213 if (!in_array($elementName, $uploadNames)) {
1214 $uploadNames[] = $elementName;
1217 $this->set('uploadNames', $uploadNames);
1219 $config = CRM_Core_Config
::singleton();
1220 if (!empty($uploadNames)) {
1221 $this->controller
->addUploadAction($config->customFileUploadDir
, $uploadNames);
1228 function buttonType() {
1229 $uploadNames = $this->get('uploadNames');
1230 $buttonType = (is_array($uploadNames) && !empty($uploadNames)) ?
'upload' : 'next';
1231 $this->assign('buttonType', $buttonType);
1240 function getVar($name) {
1241 return isset($this->$name) ?
$this->$name : NULL;
1248 function setVar($name, $value) {
1249 $this->$name = $value;
1254 * @param string $name name of the element
1255 * @param string $label label of the element
1256 * @param array $attributes key / value pair
1258 * // if you need time
1259 * $attributes = array(
1260 * 'addTime' => true,
1261 * 'formatType' => 'relative' or 'birth' etc check advanced date settings
1263 * @param boolean $required true if required
1266 function addDate($name, $label, $required = FALSE, $attributes = NULL) {
1267 if (!empty($attributes['formatType'])) {
1268 // get actual format
1269 $params = array('name' => $attributes['formatType']);
1272 // cache date information
1274 $key = "dateFormat_" . str_replace(' ', '_', $attributes['formatType']);
1275 if (empty($dateFormat[$key])) {
1276 CRM_Core_DAO
::commonRetrieve('CRM_Core_DAO_PreferencesDate', $params, $values);
1277 $dateFormat[$key] = $values;
1280 $values = $dateFormat[$key];
1283 if ($values['date_format']) {
1284 $attributes['format'] = $values['date_format'];
1287 if (!empty($values['time_format'])) {
1288 $attributes['timeFormat'] = $values['time_format'];
1290 $attributes['startOffset'] = $values['start'];
1291 $attributes['endOffset'] = $values['end'];
1294 $config = CRM_Core_Config
::singleton();
1295 if (empty($attributes['format'])) {
1296 $attributes['format'] = $config->dateInputFormat
;
1299 if (!isset($attributes['startOffset'])) {
1300 $attributes['startOffset'] = 10;
1303 if (!isset($attributes['endOffset'])) {
1304 $attributes['endOffset'] = 10;
1307 $this->add('text', $name, $label, $attributes);
1309 if (!empty($attributes['addTime']) ||
!empty($attributes['timeFormat'])) {
1311 if (!isset($attributes['timeFormat'])) {
1312 $timeFormat = $config->timeInputFormat
;
1315 $timeFormat = $attributes['timeFormat'];
1318 // 1 - 12 hours and 2 - 24 hours, but for jquery widget it is 0 and 1 respectively
1320 $show24Hours = TRUE;
1321 if ($timeFormat == 1) {
1322 $show24Hours = FALSE;
1325 //CRM-6664 -we are having time element name
1326 //in either flat string or an array format.
1327 $elementName = $name . '_time';
1328 if (substr($name, -1) == ']') {
1329 $elementName = substr($name, 0, strlen($name) - 1) . '_time]';
1332 $this->add('text', $elementName, ts('Time'), array('timeFormat' => $show24Hours));
1337 $this->addRule($name, ts('Please select %1', array(1 => $label)), 'required');
1338 if (!empty($attributes['addTime']) && !empty($attributes['addTimeRequired'])) {
1339 $this->addRule($elementName, ts('Please enter a time.'), 'required');
1345 * Function that will add date and time
1347 function addDateTime($name, $label, $required = FALSE, $attributes = NULL) {
1348 $addTime = array('addTime' => TRUE);
1349 if (is_array($attributes)) {
1350 $attributes = array_merge($attributes, $addTime);
1353 $attributes = $addTime;
1356 $this->addDate($name, $label, $required, $attributes);
1360 * Add a currency and money element to the form
1362 function addMoney($name,
1366 $addCurrency = TRUE,
1367 $currencyName = 'currency',
1368 $defaultCurrency = NULL,
1369 $freezeCurrency = FALSE
1371 $element = $this->add('text', $name, $label, $attributes, $required);
1372 $this->addRule($name, ts('Please enter a valid amount.'), 'money');
1375 $ele = $this->addCurrency($currencyName, NULL, TRUE, $defaultCurrency, $freezeCurrency);
1382 * Add currency element to the form
1384 function addCurrency($name = 'currency',
1387 $defaultCurrency = NULL,
1388 $freezeCurrency = FALSE
1390 $currencies = CRM_Core_OptionGroup
::values('currencies_enabled');
1391 $options = array('class' => 'crm-select2 eight');
1393 $currencies = array('' => '') +
$currencies;
1394 $options['placeholder'] = ts('- none -');
1396 $ele = $this->add('select', $name, $label, $currencies, $required, $options);
1397 if ($freezeCurrency) {
1400 if (!$defaultCurrency) {
1401 $config = CRM_Core_Config
::singleton();
1402 $defaultCurrency = $config->defaultCurrency
;
1404 $this->setDefaults(array($name => $defaultCurrency));
1408 * Create a single or multiple entity ref field
1409 * @param string $name
1410 * @param string $label
1411 * @param array $props mix of html and widget properties, including:
1412 * - select - params to give to select2 widget
1413 * - entity - defaults to contact
1414 * - create - can the user create a new entity on-the-fly?
1415 * Set to TRUE if entity is contact and you want the default profiles,
1416 * or pass in your own set of links. @see CRM_Core_BAO_UFGroup::getCreateLinks for format
1417 * note that permissions are checked automatically
1418 * - api - array of settings for the getlist api wrapper
1419 * note that it accepts a 'params' setting which will be passed to the underlying api
1420 * - placeholder - string
1422 * - class, etc. - other html properties
1423 * @param bool $required
1426 * @return HTML_QuickForm_Element
1428 function addEntityRef($name, $label = '', $props = array(), $required = FALSE) {
1429 require_once "api/api.php";
1430 $config = CRM_Core_Config
::singleton();
1431 // Default properties
1432 $props['api'] = CRM_Utils_Array
::value('api', $props, array());
1433 $props['entity'] = _civicrm_api_get_entity_name_from_camel(CRM_Utils_Array
::value('entity', $props, 'contact'));
1434 $props['class'] = ltrim(CRM_Utils_Array
::value('class', $props, '') . ' crm-form-entityref');
1436 if ($props['entity'] == 'contact' && isset($props['create']) && !(CRM_Core_Permission
::check('edit all contacts') || CRM_Core_Permission
::check('add contacts'))) {
1437 unset($props['create']);
1440 $props['placeholder'] = CRM_Utils_Array
::value('placeholder', $props, $required ?
ts('- select %1 -', array(1 => ts(str_replace('_', ' ', $props['entity'])))) : ts('- none -'));
1442 $defaults = array();
1443 if (!empty($props['multiple'])) {
1444 $defaults['multiple'] = TRUE;
1446 $props['select'] = CRM_Utils_Array
::value('select', $props, array()) +
$defaults;
1448 $this->formatReferenceFieldAttributes($props);
1449 return $this->add('text', $name, $label, $props, $required);
1455 private function formatReferenceFieldAttributes(&$props) {
1456 $props['data-select-params'] = json_encode($props['select']);
1457 $props['data-api-params'] = $props['api'] ?
json_encode($props['api']) : NULL;
1458 $props['data-api-entity'] = $props['entity'];
1459 if (!empty($props['create'])) {
1460 $props['data-create-links'] = json_encode($props['create']);
1462 CRM_Utils_Array
::remove($props, 'multiple', 'select', 'api', 'entity', 'create');
1466 * Convert all date fields within the params to mysql date ready for the
1467 * BAO layer. In this case fields are checked against the $_datefields defined for the form
1468 * and if time is defined it is incorporated
1470 * @param array $params input params from the form
1472 * @todo it would probably be better to work on $this->_params than a passed array
1473 * @todo standardise the format which dates are passed to the BAO layer in & remove date
1476 function convertDateFieldsToMySQL(&$params){
1477 foreach ($this->_dateFields
as $fieldName => $specs){
1478 if(!empty($params[$fieldName])){
1479 $params[$fieldName] = CRM_Utils_Date
::isoToMysql(
1480 CRM_Utils_Date
::processDate(
1481 $params[$fieldName],
1482 CRM_Utils_Array
::value("{$fieldName}_time", $params), TRUE)
1486 if(isset($specs['default'])){
1487 $params[$fieldName] = date('YmdHis', strtotime($specs['default']));
1494 * @param $elementName
1496 function removeFileRequiredRules($elementName) {
1497 $this->_required
= array_diff($this->_required
, array($elementName));
1498 if (isset($this->_rules
[$elementName])) {
1499 foreach ($this->_rules
[$elementName] as $index => $ruleInfo) {
1500 if ($ruleInfo['type'] == 'uploadedfile') {
1501 unset($this->_rules
[$elementName][$index]);
1504 if (empty($this->_rules
[$elementName])) {
1505 unset($this->_rules
[$elementName]);
1511 * Function that can be defined in Form to override or
1512 * perform specific action on cancel action
1516 function cancelAction() {}
1519 * Helper function to verify that required fields have been filled
1520 * Typically called within the scope of a FormRule function
1522 static function validateMandatoryFields($fields, $values, &$errors) {
1523 foreach ($fields as $name => $fld) {
1524 if (!empty($fld['is_required']) && CRM_Utils_System
::isNull(CRM_Utils_Array
::value($name, $values))) {
1525 $errors[$name] = ts('%1 is a required field.', array(1 => $fld['title']));
1531 * Get contact if for a form object. Prioritise
1532 * - cid in URL if 0 (on behalf on someoneelse)
1533 * (@todo consider setting a variable if onbehalf for clarity of downstream 'if's
1534 * - logged in user id if it matches the one in the cid in the URL
1535 * - contact id validated from a checksum from a checksum
1536 * - cid from the url if the caller has ACL permission to view
1537 * - fallback is logged in user (or ? NULL if no logged in user) (@todo wouldn't 0 be more intuitive?)
1539 * @return mixed NULL|integer
1541 function getContactID() {
1542 $tempID = CRM_Utils_Request
::retrieve('cid', 'Positive', $this);
1543 if(isset($this->_params
) && isset($this->_params
['select_contact_id'])) {
1544 $tempID = $this->_params
['select_contact_id'];
1546 if(isset($this->_params
, $this->_params
[0]) && !empty($this->_params
[0]['select_contact_id'])) {
1547 // event form stores as an indexed array, contribution form not so much...
1548 $tempID = $this->_params
[0]['select_contact_id'];
1551 // force to ignore the authenticated user
1552 if ($tempID === '0' ||
$tempID === 0) {
1553 // we set the cid on the form so that this will be retained for the Confirm page
1554 // in the multi-page form & prevent us returning the $userID when this is called
1556 // we don't really need to set it when $tempID is set because the params have that stored
1557 $this->set('cid', 0);
1561 $userID = $this->getLoggedInUserContactID();
1563 if ($tempID == $userID) {
1567 //check if this is a checksum authentication
1568 $userChecksum = CRM_Utils_Request
::retrieve('cs', 'String', $this);
1569 if ($userChecksum) {
1570 //check for anonymous user.
1571 $validUser = CRM_Contact_BAO_Contact_Utils
::validChecksum($tempID, $userChecksum);
1576 // check if user has permission, CRM-12062
1577 else if ($tempID && CRM_Contact_BAO_Contact_Permission
::allow($tempID)) {
1585 * Get the contact id of the logged in user
1587 function getLoggedInUserContactID() {
1588 // check if the user is logged in and has a contact ID
1589 $session = CRM_Core_Session
::singleton();
1590 return $session->get('userID');
1594 * Add autoselector field -if user has permission to view contacts
1595 * If adding this to a form you also need to add to the tpl e.g
1597 * {if !empty($selectable)}
1598 * <div class="crm-summary-row">
1599 * <div class="crm-label">{$form.select_contact.label}</div>
1600 * <div class="crm-content">
1601 * {$form.select_contact.html}
1606 * @param array $profiles ids of profiles that are on the form (to be autofilled)
1607 * @param array $autoCompleteField
1611 * - url (for ajax lookup)
1613 * @todo add data attributes so we can deal with multiple instances on a form
1615 function addAutoSelector($profiles = array(), $autoCompleteField = array()) {
1616 $autoCompleteField = array_merge(array(
1617 'id_field' => 'select_contact_id',
1618 'placeholder' => ts('Select someone else ...'),
1619 'show_hide' => TRUE,
1620 'api' => array('params' => array('contact_type' => 'Individual'))
1621 ), $autoCompleteField);
1623 if($this->canUseAjaxContactLookups()) {
1624 $this->assign('selectable', $autoCompleteField['id_field']);
1625 $this->addEntityRef($autoCompleteField['id_field'], NULL, array('placeholder' => $autoCompleteField['placeholder'], 'api' => $autoCompleteField['api']));
1627 CRM_Core_Resources
::singleton()->addScriptFile('civicrm', 'js/AlternateContactSelector.js', 1, 'html-header')
1629 'form' => array('autocompletes' => $autoCompleteField),
1630 'ids' => array('profile' => $profiles),
1638 function canUseAjaxContactLookups() {
1639 if (0 < (civicrm_api3('contact', 'getcount', array('check_permissions' => 1))) &&
1640 CRM_Core_Permission
::check(array(array('access AJAX API', 'access CiviCRM')))) {
1646 * Add the options appropriate to cid = zero - ie. autocomplete
1648 * @todo there is considerable code duplication between the contribution forms & event forms. It is apparent
1649 * that small pieces of duplication are not being refactored into separate functions because their only shared parent
1650 * is this form. Inserting a class FrontEndForm.php between the contribution & event & this class would allow functions like this
1651 * and a dozen other small ones to be refactored into a shared parent with the reduction of much code duplication
1653 function addCIDZeroOptions($onlinePaymentProcessorEnabled) {
1654 $this->assign('nocid', TRUE);
1655 $profiles = array();
1656 if($this->_values
['custom_pre_id']) {
1657 $profiles[] = $this->_values
['custom_pre_id'];
1659 if($this->_values
['custom_post_id']) {
1660 $profiles[] = $this->_values
['custom_post_id'];
1662 if($onlinePaymentProcessorEnabled) {
1663 $profiles[] = 'billing';
1665 if(!empty($this->_values
)) {
1666 $this->addAutoSelector($profiles);
1671 * Set default values on form for given contact (or no contact defaults)
1673 * @param mixed $profile_id (can be id, or profile name)
1674 * @param integer $contactID
1678 function getProfileDefaults($profile_id = 'Billing', $contactID = NULL) {
1680 $defaults = civicrm_api3('profile', 'getsingle', array(
1681 'profile_id' => (array) $profile_id,
1682 'contact_id' => $contactID,
1686 catch (Exception
$e) {
1687 // the try catch block gives us silent failure -not 100% sure this is a good idea
1688 // as silent failures are often worse than noisy ones
1694 * Sets form attribute
1697 function preventAjaxSubmit() {
1698 $this->setAttribute('data-no-ajax-submit', 'true');
1702 * Sets form attribute
1705 function allowAjaxSubmit() {
1706 $this->removeAttribute('data-no-ajax-submit');
1710 * Sets page title based on entity and action
1711 * @param string $entityLabel
1713 function setPageTitle($entityLabel) {
1714 switch ($this->_action
) {
1715 case CRM_Core_Action
::ADD
:
1716 CRM_Utils_System
::setTitle(ts('New %1', array(1 => $entityLabel)));
1718 case CRM_Core_Action
::UPDATE
:
1719 CRM_Utils_System
::setTitle(ts('Edit %1', array(1 => $entityLabel)));
1721 case CRM_Core_Action
::VIEW
:
1722 case CRM_Core_Action
::PREVIEW
:
1723 CRM_Utils_System
::setTitle(ts('View %1', array(1 => $entityLabel)));
1725 case CRM_Core_Action
::DELETE
:
1726 CRM_Utils_System
::setTitle(ts('Delete %1', array(1 => $entityLabel)));
1732 * Create a chain-select target field. All settings are optional; the defaults usually work.
1734 * @param string $elementName
1735 * @param array $settings
1737 * @return HTML_QuickForm_Element
1739 public function addChainSelect($elementName, $settings = array()) {
1740 $props = $settings +
= array(
1741 'control_field' => str_replace(array('state_province', 'StateProvince', 'county', 'County'), array('country', 'Country', 'state_province', 'StateProvince'), $elementName),
1742 'data-callback' => strpos($elementName, 'rovince') ?
'civicrm/ajax/jqState' : 'civicrm/ajax/jqCounty',
1743 'label' => strpos($elementName, 'rovince') ?
ts('State/Province') : ts('County'),
1744 'data-empty-prompt' => strpos($elementName, 'rovince') ?
ts('Choose country first') : ts('Choose state first'),
1745 'data-none-prompt' => ts('- N/A -'),
1746 'multiple' => FALSE,
1747 'required' => FALSE,
1748 'placeholder' => empty($settings['required']) ?
ts('- none -') : ts('- select -'),
1750 CRM_Utils_Array
::remove($props, 'label', 'required', 'control_field');
1751 $props['class'] = (empty($props['class']) ?
'' : "{$props['class']} ") . 'crm-select2';
1752 $props['data-select-prompt'] = $props['placeholder'];
1753 $props['data-name'] = $elementName;
1755 $this->_chainSelectFields
[$settings['control_field']] = $elementName;
1757 // Passing NULL instead of an array of options
1758 // CRM-15225 - normally QF will reject any selected values that are not part of the field's options, but due to a
1759 // quirk in our patched version of HTML_QuickForm_select, this doesn't happen if the options are NULL
1760 // which seems a bit dirty but it allows our dynamically-popuplated select element to function as expected.
1761 return $this->add('select', $elementName, $settings['label'], NULL, $settings['required'], $props);
1765 * Set options and attributes for chain select fields based on the controlling field's value
1767 private function preProcessChainSelectFields() {
1768 foreach ($this->_chainSelectFields
as $control => $target) {
1769 $targetField = $this->getElement($target);
1770 $targetType = $targetField->getAttribute('data-callback') == 'civicrm/ajax/jqCounty' ?
'county' : 'stateProvince';
1772 // If the control field is on the form, setup chain-select and dynamically populate options
1773 if ($this->elementExists($control)) {
1774 $controlField = $this->getElement($control);
1775 $controlType = $targetType == 'county' ?
'stateProvince' : 'country';
1777 $targetField->setAttribute('class', $targetField->getAttribute('class') . ' crm-chain-select-target');
1779 $css = (string) $controlField->getAttribute('class');
1780 $controlField->updateAttributes(array(
1781 'class' => ($css ?
"$css " : 'crm-select2 ') . 'crm-chain-select-control',
1782 'data-target' => $target,
1784 $controlValue = $controlField->getValue();
1785 if ($controlValue) {
1786 $options = CRM_Core_BAO_Location
::getChainSelectValues($controlValue, $controlType, TRUE);
1788 $targetField->setAttribute('placeholder', $targetField->getAttribute('data-none-prompt'));
1791 $targetField->setAttribute('placeholder', $targetField->getAttribute('data-empty-prompt'));
1792 $targetField->setAttribute('disabled', 'disabled');
1795 // Control field not present - fall back to loading default options
1797 $options = CRM_Core_PseudoConstant
::$targetType();
1799 if (!$targetField->getAttribute('multiple')) {
1800 $options = array('' => $targetField->getAttribute('placeholder')) +
$options;
1801 $targetField->removeAttribute('placeholder');
1803 $targetField->_options
= array();
1804 $targetField->loadArray($options);
1809 * Validate country / state / county match and suppress unwanted "required" errors
1811 private function validateChainSelectFields() {
1812 foreach ($this->_chainSelectFields
as $control => $target) {
1813 if ($this->elementExists($control)) {
1814 $controlValue = (array)$this->getElementValue($control);
1815 $targetField = $this->getElement($target);
1816 $controlType = $targetField->getAttribute('data-callback') == 'civicrm/ajax/jqCounty' ?
'stateProvince' : 'country';
1817 $targetValue = array_filter((array)$targetField->getValue());
1818 if ($targetValue ||
$this->getElementError($target)) {
1819 $options = CRM_Core_BAO_Location
::getChainSelectValues($controlValue, $controlType, TRUE);
1821 if (!array_intersect($targetValue, array_keys($options))) {
1822 $this->setElementError($target, $controlType == 'country' ?
ts('State/Province does not match the selected Country') : ts('County does not match the selected State/Province'));
1824 } // Suppress "required" error for field if it has no options
1825 elseif (!$options) {
1826 $this->setElementError($target, NULL);