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