3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
13 * This class acts as our base controller class and adds additional
14 * functionality and smarts to the base QFC. Specifically we create
15 * our own action classes and handle the transitions ourselves by
16 * simulating a state machine. We also create direct jump links to any
17 * page that can be used universally.
19 * This concept has been discussed on the PEAR list and the QFC FAQ
20 * goes into a few details. Please check
21 * http://pear.php.net/manual/en/package.html.html-quickform-controller.faq.php
22 * for other useful tips and suggestions
25 * @copyright CiviCRM LLC https://civicrm.org/licensing
28 require_once 'HTML/QuickForm/Controller.php';
29 require_once 'HTML/QuickForm/Action/Direct.php';
32 * Class CRM_Core_Controller
34 class CRM_Core_Controller
extends HTML_QuickForm_Controller
{
37 * The title associated with this controller.
44 * The key associated with this controller.
51 * The name of the session scope where values are stored.
58 * The state machine associated with this controller.
62 protected $_stateMachine;
65 * Is this object being embedded in another object. If
66 * so the display routine needs to not do any work. (The
67 * parent object takes care of the display)
71 protected $_embedded = FALSE;
74 * After entire form execution complete,
75 * do we want to skip control redirection.
76 * Default - It get redirect to user context.
78 * Useful when we run form in non civicrm context
79 * and we need to transfer control back.(eg. drupal)
83 protected $_skipRedirection = FALSE;
86 * Are we in print mode? if so we need to modify the display
87 * functionality to do a minimal display :)
94 * Should we generate a qfKey, true by default
98 public $_generateQFKey = TRUE;
105 public $_QFResponseType = 'html';
108 * Cache the smarty template for efficiency reasons.
110 * @var CRM_Core_Smarty
112 static protected $_template;
115 * Cache the session for efficiency reasons.
117 * @var CRM_Core_Session
119 static protected $_session;
122 * The parent of this form if embedded.
126 protected $_parent = NULL;
129 * The destination if set will override the destination the code wants to send it to.
133 public $_destination = NULL;
136 * The entry url for a top level form or wizard. Typically the URL with a reset=1
137 * used to redirect back to when we land into some session wierdness
141 public $_entryURL = NULL;
144 * All CRM single or multi page pages should inherit from this class.
146 * @param string $title
147 * Descriptive title of the controller.
149 * Whether controller is modal.
151 * @param string $scope
152 * Name of session if we want unique scope, used only by Controller_Simple.
153 * @param bool $addSequence
154 * Should we add a unique sequence number to the end of the key.
155 * @param bool $ignoreKey
156 * Should we not set a qfKey for this controller (for standalone forms).
158 * @throws \CRM_Core_Exception
160 public function __construct(
165 $addSequence = FALSE,
168 // this has to true for multiple tab session fix
171 // let the constructor initialize this, should happen only once
172 if (!isset(self
::$_template)) {
173 self
::$_template = CRM_Core_Smarty
::singleton();
174 self
::$_session = CRM_Core_Session
::singleton();
177 // lets try to get it from the session and/or the request vars
178 // we do this early on in case there is a fatal error in retrieving the
179 // key and/or session
181 = CRM_Utils_Request
::retrieve('entryURL', 'String', $this);
183 // add a unique validable key to the name
184 $name = CRM_Utils_System
::getClassName($this);
185 if ($name == 'CRM_Core_Controller_Simple' && !empty($scope)) {
186 // use form name if we have, since its a lot better and
187 // definitely different for different forms
190 $name = $name . '_' . $this->key($name, $addSequence, $ignoreKey);
191 $this->_title
= $title;
193 $this->_scope
= $scope;
196 $this->_scope
= CRM_Utils_System
::getClassName($this);
198 $this->_scope
= $this->_scope
. '_' . $this->_key
;
200 // only use the civicrm cache if we have a valid key
201 // else we clash with other users CRM-7059
202 if (!empty($this->_key
)) {
203 CRM_Core_Session
::registerAndRetrieveSessionObjects([
204 "_{$name}_container",
205 ['CiviCRM', $this->_scope
],
209 parent
::__construct($name, $modal);
211 $snippet = $_REQUEST['snippet'] ??
NULL;
214 $this->_print
= CRM_Core_Smarty
::PRINT_PDF
;
216 elseif ($snippet == 4) {
217 // this is used to embed fragments of a form
218 $this->_print
= CRM_Core_Smarty
::PRINT_NOFORM
;
219 self
::$_template->assign('suppressForm', TRUE);
220 $this->_generateQFKey
= FALSE;
222 elseif ($snippet == 5) {
223 // mode deprecated in favor of json
224 // still used by dashlets, probably nothing else
225 $this->_print
= CRM_Core_Smarty
::PRINT_NOFORM
;
227 // Respond with JSON if in AJAX context (also support legacy value '6')
228 elseif (in_array($snippet, [CRM_Core_Smarty
::PRINT_JSON
, 6])) {
229 $this->_print
= CRM_Core_Smarty
::PRINT_JSON
;
230 $this->_QFResponseType
= 'json';
233 $this->_print
= CRM_Core_Smarty
::PRINT_SNIPPET
;
237 // if the request has a reset value, initialize the controller session
238 if (!empty($_GET['reset'])) {
241 // in this case we'll also cache the url as a hidden form variable, this allows us to
242 // redirect in case the session has disappeared on us
243 $this->_entryURL
= CRM_Utils_System
::makeURL(NULL, TRUE, FALSE, NULL, TRUE);
244 // In WordPress Shortcodes the standard entryURL generated via makeURL doesn't generally have id=x&reset=1 included so we add them here
245 // This prevents infinite loops caused when the session has timed out.
246 if (stripos($this->_entryURL
, 'id') === FALSE && (stripos($this->_entryURL
, 'transact') !== FALSE ||
stripos($this->_entryURL
, 'register') !== FALSE)) {
247 $this->_entryURL
.= '&id=' . CRM_Utils_Request
::retrieveValue('id', 'Positive') . '&reset=1';
249 $this->set('entryURL', $this->_entryURL
);
252 // set the key in the session
253 // do this at the end so we have initialized the object
254 // and created the scope etc
255 $this->set('qfKey', $this->_key
);
257 // also retrieve and store destination in session
258 $this->_destination
= CRM_Utils_Request
::retrieve(
259 'civicrmDestination',
268 public function fini() {
269 CRM_Core_BAO_Cache
::storeSessionToCache([
270 "_{$this->_name}_container",
271 ['CiviCRM', $this->_scope
],
276 * @param string $name
277 * @param bool $addSequence
278 * @param bool $ignoreKey
280 * @return mixed|string
282 public function key($name, $addSequence = FALSE, $ignoreKey = FALSE) {
283 $config = CRM_Core_Config
::singleton();
287 (isset($config->keyDisable
) && $config->keyDisable
)
292 // We need a form key. Check _POST first, then _GET.
293 // @todo Note: we currently have to check $_REQUEST, too, since that
294 // is currently overwritten by civicrm_api3_contribution_page_validate.
295 // It's bad form to use $_REQUEST because it's ambiguous; and it's bad form
296 // to change superglobals anyway. If PR
297 // https://github.com/civicrm/civicrm-core/pull/17324
298 // and/or related get merged, then we should remove the REQUEST reference here.
299 $key = $_POST['qfKey'] ??
$_GET['qfKey'] ??
$_REQUEST['qfKey'] ??
NULL;
300 if (!$key && in_array($_SERVER['REQUEST_METHOD'], ['GET', 'HEAD'])) {
301 // Generate a key if this is an initial request without one.
302 // We allow HEAD here because it is used by bots to validate URLs, so if
303 // we issue a 500 server error to them they may think the site is broken.
304 $key = CRM_Core_Key
::get($name, $addSequence);
307 // Other requests that usually change data (POST, but feasibly DELETE,
308 // PUT, PATCH...) always require a valid key.
309 $key = CRM_Core_Key
::validate($key, $name, $addSequence);
322 * Process the request, overrides the default QFC run method
323 * This routine actually checks if the QFC is modal and if it
324 * is the first invalid page, if so it call the requested action
325 * if not, it calls the display action on the first invalid page
326 * avoids the issue of users hitting the back button and getting
329 * This run is basically a composition of the original run and the
334 public function run() {
335 // the names of the action and page should be saved
336 // note that this is split into two, because some versions of
337 // php 5.x core dump on the triple assignment :)
338 $this->_actionName
= $this->getActionName();
339 list($pageName, $action) = $this->_actionName
;
341 if ($this->isModal()) {
342 if (!$this->isValid($pageName)) {
343 $pageName = $this->findInvalid();
348 // note that based on action, control might not come back!!
349 // e.g. if action is a valid JUMP, u basically do a redirect
350 // to the appropriate place
351 $this->wizardHeader($pageName);
352 return $this->_pages
[$pageName]->handle($action);
358 public function validate() {
359 $this->_actionName
= $this->getActionName();
360 list($pageName, $action) = $this->_actionName
;
362 $page = &$this->_pages
[$pageName];
364 $data = &$this->container();
365 $this->applyDefaults($pageName);
366 $page->isFormBuilt() or $page->buildForm();
367 // We use defaults and constants as if they were submitted
368 $data['values'][$pageName] = $page->exportValues();
369 $page->loadValues($data['values'][$pageName]);
370 // Is the page now valid?
371 if (TRUE === ($data['valid'][$pageName] = $page->validate())) {
374 return $page->_errors
;
378 * Helper function to add all the needed default actions.
380 * Note that the framework redefines all of the default QFC actions.
382 * @param string $uploadDirectory to store all the uploaded files
383 * @param array $uploadNames for the various upload buttons (note u can have more than 1 upload)
385 public function addActions($uploadDirectory = NULL, $uploadNames = NULL) {
387 'display' => 'CRM_Core_QuickForm_Action_Display',
388 'next' => 'CRM_Core_QuickForm_Action_Next',
389 'back' => 'CRM_Core_QuickForm_Action_Back',
390 'process' => 'CRM_Core_QuickForm_Action_Process',
391 'cancel' => 'CRM_Core_QuickForm_Action_Cancel',
392 'refresh' => 'CRM_Core_QuickForm_Action_Refresh',
393 'reload' => 'CRM_Core_QuickForm_Action_Reload',
394 'done' => 'CRM_Core_QuickForm_Action_Done',
395 'jump' => 'CRM_Core_QuickForm_Action_Jump',
396 'submit' => 'CRM_Core_QuickForm_Action_Submit',
399 foreach ($names as $name => $classPath) {
400 $action = new $classPath($this->_stateMachine
);
401 $this->addAction($name, $action);
404 $this->addUploadAction($uploadDirectory, $uploadNames);
408 * Getter method for stateMachine.
410 * @return CRM_Core_StateMachine
412 public function getStateMachine() {
413 return $this->_stateMachine
;
417 * Setter method for stateMachine.
419 * @param CRM_Core_StateMachine $stateMachine
421 public function setStateMachine($stateMachine) {
422 $this->_stateMachine
= $stateMachine;
426 * Add pages to the controller. Note that the controller does not really care
427 * the order in which the pages are added
429 * @param CRM_Core_StateMachine $stateMachine
430 * @param \const|int $action the mode in which the state machine is operating
431 * typically this will be add/view/edit
433 public function addPages(&$stateMachine, $action = CRM_Core_Action
::NONE
) {
434 $pages = $stateMachine->getPages();
435 foreach ($pages as $name => $value) {
436 $className = CRM_Utils_Array
::value('className', $value, $name);
437 $title = $value['title'] ??
NULL;
438 $options = $value['options'] ??
NULL;
439 $stateName = CRM_Utils_String
::getClassName($className);
440 if (!empty($value['className'])) {
444 $formName = CRM_Utils_String
::getClassName($name);
447 $ext = CRM_Extension_System
::singleton()->getMapper();
448 if ($ext->isExtensionClass($className)) {
449 require_once $ext->classToPath($className);
452 require_once str_replace('_', DIRECTORY_SEPARATOR
, $className) . '.php';
454 $
$stateName = new $className($stateMachine->find($className), $action, 'post', $formName);
456 $
$stateName->setTitle($title);
459 $
$stateName->setOptions($options);
461 if (property_exists($
$stateName, 'urlPath')) {
462 $
$stateName->urlPath
= explode('/', (string) CRM_Utils_System
::currentPath());
464 $this->addPage($
$stateName);
465 $this->addAction($stateName, new HTML_QuickForm_Action_Direct());
467 //CRM-6342 -we need kill the reference here,
468 //as we have deprecated reference object creation.
474 * QFC does not provide native support to have different 'submit' buttons.
475 * We introduce this notion to QFC by using button specific data. Thus if
476 * we have two submit buttons, we could have one displayed as a button and
477 * the other as an image, both are of type 'submit'.
480 * the name of the button that has been pressed by the user
482 public function getButtonName() {
483 $data = &$this->container();
484 return $data['_qf_button_name'] ??
NULL;
488 * Destroy all the session state of the controller.
490 public function reset() {
491 $this->container(TRUE);
492 self
::$_session->resetScope($this->_scope
);
496 * Virtual function to do any processing of data.
498 * Sometimes it is useful for the controller to actually process data.
499 * This is typically used when we need the controller to figure out
500 * what pages are potentially involved in this wizard. (this is dynamic
501 * and can change based on the arguments
503 public function process() {
507 * Store the variable with the value in the form scope.
509 * @param string|array $name name of the variable or an assoc array of name/value pairs
510 * @param mixed $value
511 * Value of the variable if string.
513 public function set($name, $value = NULL) {
514 self
::$_session->set($name, $value, $this->_scope
);
518 * Get the variable from the form scope.
520 * @param string $name
521 * name of the variable.
525 public function get($name) {
526 return self
::$_session->get($name, $this->_scope
);
530 * Create the header for the wizard from the list of pages.
531 * Store the created header in smarty
533 * @param string $currentPageName
534 * Name of the page being displayed.
538 public function wizardHeader($currentPageName) {
540 $wizard['steps'] = [];
542 foreach ($this->_pages
as $name => $page) {
544 $wizard['steps'][] = [
546 'title' => $page->getTitle(),
547 //'link' => $page->getLink ( ),
551 'stepNumber' => $count,
552 'collapsed' => FALSE,
555 if ($name == $currentPageName) {
556 $wizard['currentStepNumber'] = $count;
557 $wizard['currentStepName'] = $name;
558 $wizard['currentStepTitle'] = $page->getTitle();
562 $wizard['stepCount'] = $count;
564 $this->addWizardStyle($wizard);
566 $this->assign('wizard', $wizard);
571 * @param array $wizard
573 public function addWizardStyle(&$wizard) {
576 'stepPrefixCurrent' => '<i class="crm-i fa-chevron-right" aria-hidden="true"></i> ',
577 'stepPrefixPast' => '<i class="crm-i fa-check" aria-hidden="true"></i> ',
578 'stepPrefixFuture' => ' ',
579 'subStepPrefixCurrent' => ' ',
580 'subStepPrefixPast' => ' ',
581 'subStepPrefixFuture' => ' ',
587 * Assign value to name in template.
590 * @param mixed $value
593 public function assign($var, $value = NULL) {
594 self
::$_template->assign($var, $value);
598 * Assign value to name in template by reference.
601 * @param mixed $value
602 * (reference) value of variable.
604 public function assign_by_ref($var, &$value) {
605 self
::$_template->assign_by_ref($var, $value);
609 * Appends values to template variables.
611 * @param array|string $tpl_var the template variable name(s)
612 * @param mixed $value
613 * The value to append.
616 public function append($tpl_var, $value = NULL, $merge = FALSE) {
617 self
::$_template->append($tpl_var, $value, $merge);
621 * Returns an array containing template variables.
623 * @param string $name
627 public function get_template_vars($name = NULL) {
628 return self
::$_template->get_template_vars($name);
632 * Setter for embedded.
634 * @param bool $embedded
636 public function setEmbedded($embedded) {
637 $this->_embedded
= $embedded;
641 * Getter for embedded.
644 * return the embedded value
646 public function getEmbedded() {
647 return $this->_embedded
;
651 * Setter for skipRedirection.
653 * @param bool $skipRedirection
655 public function setSkipRedirection($skipRedirection) {
656 $this->_skipRedirection
= $skipRedirection;
660 * Getter for skipRedirection.
663 * return the skipRedirection value
665 public function getSkipRedirection() {
666 return $this->_skipRedirection
;
670 * @param null $fileName
672 public function setWord($fileName = NULL) {
673 //Mark as a CSV file.
674 CRM_Utils_System
::setHttpHeader('Content-Type', 'application/vnd.ms-word');
676 //Force a download and name the file using the current timestamp.
678 $fileName = 'Contacts_' . $_SERVER['REQUEST_TIME'] . '.doc';
680 CRM_Utils_System
::setHttpHeader("Content-Disposition", "attachment; filename=Contacts_$fileName");
684 * @param null $fileName
686 public function setExcel($fileName = NULL) {
687 //Mark as an excel file.
688 CRM_Utils_System
::setHttpHeader('Content-Type', 'application/vnd.ms-excel');
690 //Force a download and name the file using the current timestamp.
692 $fileName = 'Contacts_' . $_SERVER['REQUEST_TIME'] . '.xls';
695 CRM_Utils_System
::setHttpHeader("Content-Disposition", "attachment; filename=Contacts_$fileName");
703 public function setPrint($print) {
704 if ($print == "xls") {
707 elseif ($print == "doc") {
710 $this->_print
= $print;
717 * return the print value
719 public function getPrint() {
720 return $this->_print
;
726 public function getTemplateFile() {
728 if ($this->_print
== CRM_Core_Smarty
::PRINT_PAGE
) {
729 return 'CRM/common/print.tpl';
731 elseif ($this->_print
== 'xls' ||
$this->_print
== 'doc') {
732 return 'CRM/Contact/Form/Task/Excel.tpl';
735 return 'CRM/common/snippet.tpl';
739 $config = CRM_Core_Config
::singleton();
740 return 'CRM/common/' . strtolower($config->userFramework
) . '.tpl';
746 * @param $uploadNames
748 public function addUploadAction($uploadDir, $uploadNames) {
749 if (empty($uploadDir)) {
750 $config = CRM_Core_Config
::singleton();
751 $uploadDir = $config->uploadDir
;
754 if (empty($uploadNames)) {
755 $uploadNames = $this->get('uploadNames');
756 if (!empty($uploadNames)) {
757 $uploadNames = array_merge($uploadNames,
758 CRM_Core_BAO_File
::uploadNames()
762 $uploadNames = CRM_Core_BAO_File
::uploadNames();
766 $action = new CRM_Core_QuickForm_Action_Upload($this->_stateMachine
,
770 $this->addAction('upload', $action);
776 public function setParent($parent) {
777 $this->_parent
= $parent;
783 public function getParent() {
784 return $this->_parent
;
790 public function getDestination() {
791 return $this->_destination
;
796 * @param bool $setToReferer
798 public function setDestination($url = NULL, $setToReferer = FALSE) {
801 $url = $_SERVER['HTTP_REFERER'];
804 $config = CRM_Core_Config
::singleton();
805 $url = $config->userFrameworkBaseURL
;
809 $this->_destination
= $url;
810 $this->set('civicrmDestination', $this->_destination
);
816 public function cancelAction() {
817 $actionName = $this->getActionName();
818 list($pageName, $action) = $actionName;
819 return $this->_pages
[$pageName]->cancelAction();
823 * Write a simple fatal error message.
825 * Other controllers can decide to do something else and present the user a better message
826 * and/or redirect to the same page with a reset url
828 public function invalidKey() {
829 self
::invalidKeyCommon();
832 public function invalidKeyCommon() {
833 $msg = ts("We can't load the requested web page. This page requires cookies to be enabled in your browser settings. Please check this setting and enable cookies (if they are not enabled). Then try again. If this error persists, contact the site administrator for assistance.") . '<br /><br />' . ts('Site Administrators: This error may indicate that users are accessing this page using a domain or URL other than the configured Base URL. EXAMPLE: Base URL is http://example.org, but some users are accessing the page via http://www.example.org or a domain alias like http://myotherexample.org.') . '<br /><br />' . ts('Error type: Could not find a valid session key.');
834 throw new CRM_Core_Exception($msg);
838 * Instead of outputting a fatal error message, we'll just redirect
839 * to the entryURL if present
841 public function invalidKeyRedirect() {
842 if ($this->_entryURL
&& $url_parts = parse_url($this->_entryURL
)) {
843 // CRM-16832: Ensure local redirects only.
844 if (!empty($url_parts['path'])) {
845 // Prepend a slash, but don't duplicate it.
846 $redirect_url = '/' . ltrim($url_parts['path'], '/');
847 if (!empty($url_parts['query'])) {
848 $redirect_url .= '?' . $url_parts['query'];
850 CRM_Core_Session
::setStatus(ts('Your browser session has expired and we are unable to complete your form submission. We have returned you to the initial step so you can complete and resubmit the form. If you experience continued difficulties, please contact us for assistance.'));
851 return CRM_Utils_System
::redirect($redirect_url);
854 self
::invalidKeyCommon();