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