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