4 * Base class for UF system integrations
6 abstract class CRM_Utils_System_Base
{
9 * Deprecated property to check if this is a drupal install.
11 * The correct method is to have functions on the UF classes for all UF specific
12 * functions and leave the codebase oblivious to the type of CMS
16 * TRUE, if the CMS is Drupal.
18 public $is_drupal = FALSE;
21 * Deprecated property to check if this is a joomla install. The correct method is to have functions on the UF classes for all UF specific
22 * functions and leave the codebase oblivious to the type of CMS
26 * TRUE, if the CMS is Joomla!.
28 public $is_joomla = FALSE;
31 * deprecated property to check if this is a wordpress install. The correct method is to have functions on the UF classes for all UF specific
32 * functions and leave the codebase oblivious to the type of CMS
36 * TRUE, if the CMS is WordPress.
38 public $is_wordpress = FALSE;
41 * Does this CMS / UF support a CMS specific logging mechanism?
43 * @todo - we should think about offering up logging mechanisms in a way that is also extensible by extensions
45 public $supports_UF_Logging = FALSE;
49 * TRUE, if the CMS allows CMS forms to be extended by hooks.
51 public $supports_form_extensions = FALSE;
53 public function initialize() {
54 if (\CRM_Utils_System
::isSSL()) {
55 $this->mapConfigToSSL();
59 abstract public function loadBootStrap($params = [], $loadUser = TRUE, $throwError = TRUE, $realPath = NULL);
62 * Append an additional breadcrumb tag to the existing breadcrumb.
64 * @param array $breadCrumbs
66 public function appendBreadCrumb($breadCrumbs) {
70 * Reset an additional breadcrumb tag to the existing breadcrumb.
72 public function resetBreadCrumb() {
76 * Append a string to the head of the html file.
79 * The new string to be appended.
81 public function addHTMLHead($head) {
85 * Rewrite various system urls to https.
87 public function mapConfigToSSL() {
88 // dont need to do anything, let CMS handle their own switch to SSL
92 * Figure out the post url for QuickForm.
94 * @param string $action
95 * The default url if one is pre-specified.
98 * The url to post the form.
100 public function postURL($action) {
101 $config = CRM_Core_Config
::singleton();
102 if (!empty($action)) {
106 return $this->url(CRM_Utils_Array
::value($config->userFrameworkURLVar
, $_GET),
107 NULL, TRUE, NULL, FALSE
112 * Generate the url string to a CiviCRM path.
114 * @param string $path
115 * The path being linked to, such as "civicrm/add".
116 * @param string $query
117 * A query string to append to the link.
118 * @param bool $absolute
119 * Whether to force the output to be an absolute link (beginning with http).
120 * Useful for links that will be displayed outside the site, such as in an RSS feed.
121 * @param string $fragment
122 * A fragment identifier (named anchor) to append to the link.
123 * @param bool $frontend
124 * This link should be to the CMS front end (applies to WP & Joomla).
125 * @param bool $forceBackend
126 * This link should be to the CMS back end (applies to WP & Joomla).
127 * @param bool $htmlize
128 * Whether to encode special html characters such as &.
138 $forceBackend = FALSE,
145 * Return the Notification URL for Payments.
147 * The default is to pass the params through to `url()`. However the WordPress
148 * CMS class overrides this method because Notification URLs must always target
149 * the Base Page to avoid IPN failures when Forms are embedded in pages that
150 * require authentication.
152 * @param string $path
153 * The path being linked to, such as "civicrm/add".
154 * @param string $query
155 * A query string to append to the link.
156 * @param bool $absolute
157 * Whether to force the output to be an absolute link (beginning with http).
158 * Useful for links that will be displayed outside the site, such as in an RSS feed.
159 * @param string $fragment
160 * A fragment identifier (named anchor) to append to the link.
161 * @param bool $frontend
162 * This link should be to the CMS front end (applies to WP & Joomla).
163 * @param bool $forceBackend
164 * This link should be to the CMS back end (applies to WP & Joomla).
165 * @param bool $htmlize
166 * Whether to encode special html characters such as &.
169 * The Notification URL.
171 public function getNotifyUrl(
177 $forceBackend = FALSE,
180 return $this->url($path, $query, $absolute, $fragment, $frontend, $forceBackend, $htmlize);
184 * Authenticate the user against the CMS db.
186 * @param string $name
188 * @param string $password
189 * The password for the above user.
190 * @param bool $loadCMSBootstrap
191 * Load cms bootstrap?.
192 * @param string $realPath
196 * [contactID, ufID, unique string] else false if no auth
197 * @throws \CRM_Core_Exception.
199 public function authenticate($name, $password, $loadCMSBootstrap = FALSE, $realPath = NULL) {
204 * Set a message in the CMS to display to a user.
206 * @param string $message
207 * The message to set.
209 public function setMessage($message) {
213 * Load user into session.
219 public function loadUser($user) {
224 * Immediately stop script execution and display a 401 "Access Denied" page.
225 * @throws \CRM_Core_Exception
227 public function permissionDenied() {
228 throw new CRM_Core_Exception(ts('You do not have permission to access this page.'));
232 * Immediately stop script execution, log out the user and redirect to the home page.
235 * This function should be removed in favor of linking to the CMS's logout page
237 public function logout() {
241 * Clear CMS caches related to the user registration/profile forms.
242 * Used when updating/embedding profiles on CMS user forms.
245 public function updateCategories() {
249 * Get the locale set in the CMS.
251 * @return string|null
252 * Locale or null for none
254 public function getUFLocale() {
259 * If we are using a theming system, invoke theme, else just print the content.
261 * @param string $content
262 * The content that will be themed.
264 * Are we displaying to the screen or bypassing theming?.
265 * @param bool $maintenance
266 * For maintenance mode.
269 * @return string|null
270 * NULL, If $print is FALSE, and some other criteria match up.
271 * The themed string, otherwise.
273 * @todo The return value is inconsistent.
274 * @todo Better to always return, and never print.
276 public function theme(&$content, $print = FALSE, $maintenance = FALSE) {
279 // TODO: Split up; this was copied verbatim from CiviCRM 4.0's multi-UF theming function
280 // but the parts should be copied into cleaner subclass implementations
281 $config = CRM_Core_Config
::singleton();
283 $config->userSystem
->is_drupal
&&
284 function_exists('theme') &&
288 drupal_set_breadcrumb('');
289 drupal_maintenance_theme();
290 if ($region = CRM_Core_Region
::instance('html-header', FALSE)) {
291 CRM_Utils_System
::addHTMLHead($region->render(''));
293 print theme('maintenance_page', ['content' => $content]);
296 // TODO: Figure out why D7 returns but everyone else prints
303 CRM_Core_Config
::singleton()->userFramework
== 'WordPress'
305 if (!function_exists('is_admin')) {
306 throw new \
Exception('Function "is_admin()" is missing, even though WordPress is the user framework.');
308 if (!defined('ABSPATH')) {
309 throw new \
Exception('Constant "ABSPATH" is not defined, even though WordPress is the user framework.');
312 require_once ABSPATH
. 'wp-admin/admin-header.php';
315 // FIXME: we need to figure out to replace civicrm content on the frontend pages
331 public function getDefaultBlockLocation() {
336 * Get the absolute path to the site's base url.
338 * @return bool|mixed|string
340 public function getAbsoluteBaseURL() {
341 if (!defined('CIVICRM_UF_BASEURL')) {
345 $url = CRM_Utils_File
::addTrailingSlash(CIVICRM_UF_BASEURL
, '/');
347 //format url for language negotiation, CRM-7803
348 $url = $this->languageNegotiationURL($url);
350 if (CRM_Utils_System
::isSSL()) {
351 $url = str_replace('http://', 'https://', $url);
358 * Get the relative path to the sites base url.
360 * @return string|false
362 public function getRelativeBaseURL() {
363 $absoluteBaseURL = $this->getAbsoluteBaseURL();
364 if ($absoluteBaseURL === FALSE) {
367 $parts = parse_url($absoluteBaseURL);
368 return $parts['path'];
369 //$this->useFrameworkRelativeBase = empty($base['path']) ? '/' : $base['path'];
377 public function getVersion() {
382 * Format the url as per language Negotiation.
385 * @param bool $addLanguagePart
386 * @param bool $removeLanguagePart
391 public function languageNegotiationURL(
393 $addLanguagePart = TRUE,
394 $removeLanguagePart = FALSE
400 * Determine the location of the CMS root.
402 * @return string|null
403 * Local file system path to CMS root, or NULL if it cannot be determined
405 public function cmsRootPath() {
410 * Create a user in the CMS.
412 * @param array $params
413 * @param string $mail
414 * Email id for cms user.
417 * uid if user exists, false otherwise
419 public function createUser(&$params, $mail) {
424 * Update a user's email address in the CMS.
428 * @param string $email
429 * Primary contact email address.
431 public function updateCMSName($ufID, $email) {
435 * Check if user is logged in to the CMS.
439 public function isUserLoggedIn() {
444 * Check if user registration is permitted.
448 public function isUserRegistrationPermitted() {
453 * Check if user can create passwords or is initially assigned a system-generated one.
457 public function isPasswordUserGenerated() {
462 * Is a front end page being accessed.
464 * Generally this would be a contribution form or other public page as opposed to a backoffice page (like contact edit).
466 * @todo Drupal uses the is_public setting - clarify & rationalise. See https://github.com/civicrm/civicrm-drupal/pull/546/files
470 public function isFrontEndPage() {
471 return CRM_Core_Config
::singleton()->userFrameworkFrontend
;
475 * Get user login URL for hosting CMS (method declared in each CMS system class)
477 * @param string $destination
478 * If present, add destination to querystring (works for Drupal and WordPress only).
481 * loginURL for the current CMS
483 abstract public function getLoginURL($destination = '');
486 * Get the login destination string.
488 * When this is passed in the URL the user will be directed to it after filling in the CMS form.
490 * @param CRM_Core_Form $form
491 * Form object representing the 'current' form - to which the user will be returned.
493 * @return string|NULL
494 * destination value for URL
496 public function getLoginDestination(&$form) {
501 * Determine the native ID of the CMS user.
503 * @param string $username
505 * @throws CRM_Core_Exception
507 public function getUfId($username) {
508 $className = get_class($this);
509 throw new CRM_Core_Exception("Not implemented: {$className}->getUfId");
513 * Set the localisation from the user framework.
515 * @param string $civicrm_language
519 public function setUFLocale($civicrm_language) {
524 * Set a init session with user object.
527 * Array with user specific data
529 public function setUserSession($data) {
530 list($userID, $ufID) = $data;
531 $session = CRM_Core_Session
::singleton();
532 $session->set('ufID', $ufID);
533 $session->set('userID', $userID);
537 * Reset any system caches that may be required for proper CiviCRM integration.
539 public function flush() {
544 * Flush css/js caches.
546 public function clearResourceCache() {
553 * Note: This function is not to be called directly
554 * @see CRM_Core_Region::render()
556 * @param string $url absolute path to file
557 * @param string $region
558 * location within the document: 'html-header', 'page-header', 'page-footer'.
561 * TRUE if we support this operation in this CMS, FALSE otherwise
563 public function addScriptUrl($url, $region) {
568 * Add an inline script.
570 * Note: This function is not to be called directly
571 * @see CRM_Core_Region::render()
573 * @param string $code javascript code
574 * @param string $region
575 * location within the document: 'html-header', 'page-header', 'page-footer'.
578 * TRUE if we support this operation in this CMS, FALSE otherwise
580 public function addScript($code, $region) {
587 * Note: This function is not to be called directly
588 * @see CRM_Core_Region::render()
590 * @param string $url absolute path to file
591 * @param string $region
592 * location within the document: 'html-header', 'page-header', 'page-footer'.
595 * TRUE if we support this operation in this CMS, FALSE otherwise
597 public function addStyleUrl($url, $region) {
602 * Add an inline style.
604 * Note: This function is not to be called directly
605 * @see CRM_Core_Region::render()
607 * @param string $code css code
608 * @param string $region
609 * location within the document: 'html-header', 'page-header', 'page-footer'.
612 * TRUE if we support this operation in this CMS, FALSE otherwise
614 public function addStyle($code, $region) {
619 * Sets the title of the page.
621 * @param string $title
622 * Title to set in html header
623 * @param string|null $pageTitle
624 * Title to set in html body (if different)
626 public function setTitle($title, $pageTitle = NULL) {
630 * Return default Site Settings.
635 * - $url, (Joomla - non admin url)
639 public function getDefaultSiteSettings($dir) {
640 $config = CRM_Core_Config
::singleton();
641 $url = $config->userFrameworkBaseURL
;
642 return [$url, NULL, NULL];
646 * Determine the default location for file storage.
649 * 1. This was pulled out from a bigger function. It should be split
650 * into even smaller pieces and marked abstract.
651 * 2. This would be easier to compute by a calling a CMS API, but
652 * for whatever reason Civi gets it from config data.
655 * - url: string. ex: "http://example.com/sites/foo.com/files/civicrm"
656 * - path: string. ex: "/var/www/sites/foo.com/files/civicrm"
658 public function getDefaultFileStorage() {
659 global $civicrm_root;
660 $config = CRM_Core_Config
::singleton();
661 $baseURL = CRM_Utils_System
::languageNegotiationURL($config->userFrameworkBaseURL
, FALSE, TRUE);
666 if ($config->userFramework
== 'Joomla') {
668 // we need to remove the administrator/ from the end
669 $tempURL = str_replace("/administrator/", "/", $baseURL);
670 $filesURL = $tempURL . "media/civicrm/";
672 elseif ($config->userFramework
== 'UnitTests') {
673 $filesURL = $baseURL . "sites/default/files/civicrm/";
676 throw new CRM_Core_Exception("Failed to locate default file storage ($config->userFramework)");
681 'path' => CRM_Utils_File
::baseFilePath(),
686 * Determine the location of the CiviCRM source tree.
689 * 1. This was pulled out from a bigger function. It should be split
690 * into even smaller pieces and marked abstract.
691 * 2. This would be easier to compute by a calling a CMS API, but
692 * for whatever reason we take the hard way.
695 * - url: string. ex: "http://example.com/sites/all/modules/civicrm"
696 * - path: string. ex: "/var/www/sites/all/modules/civicrm"
698 public function getCiviSourceStorage() {
699 global $civicrm_root;
700 $config = CRM_Core_Config
::singleton();
702 // Don't use $config->userFrameworkBaseURL; it has garbage on it.
703 // More generally, w shouldn't be using $config here.
704 if (!defined('CIVICRM_UF_BASEURL')) {
705 throw new RuntimeException('Undefined constant: CIVICRM_UF_BASEURL');
707 $baseURL = CRM_Utils_File
::addTrailingSlash(CIVICRM_UF_BASEURL
, '/');
708 if (CRM_Utils_System
::isSSL()) {
709 $baseURL = str_replace('http://', 'https://', $baseURL);
712 // @todo this function is only called / code is only reached when is_drupal is true - move this to the drupal classes
713 // and don't implement here.
714 if ($this->is_drupal
) {
716 // check and see if we are installed in sites/all (for D5 and above)
717 // we dont use checkURL since drupal generates an error page and throws
718 // the system for a loop on lobo's macosx box
720 $cmsPath = $config->userSystem
->cmsRootPath();
721 $userFrameworkResourceURL = $baseURL . str_replace("$cmsPath/", '',
722 str_replace('\\', '/', $civicrm_root)
725 $siteName = $config->userSystem
->parseDrupalSiteNameFromRoot($civicrm_root);
727 $civicrmDirName = trim(basename($civicrm_root));
728 $userFrameworkResourceURL = $baseURL . "sites/$siteName/modules/$civicrmDirName/";
732 $userFrameworkResourceURL = NULL;
736 'url' => CRM_Utils_File
::addTrailingSlash($userFrameworkResourceURL, '/'),
737 'path' => CRM_Utils_File
::addTrailingSlash($civicrm_root),
742 * Perform any post login activities required by the CMS.
744 * e.g. for drupal: records a watchdog message about the new session, saves the login timestamp,
745 * calls hook_user op 'login' and generates a new session.
747 * @param array $params
749 * FIXME: Document values accepted/required by $params
751 public function userLoginFinalize($params = []) {
755 * Set timezone in mysql so that timestamp fields show the correct time.
757 public function setMySQLTimeZone() {
758 $timeZoneOffset = $this->getTimeZoneOffset();
759 if ($timeZoneOffset) {
760 $sql = "SET time_zone = '$timeZoneOffset'";
761 CRM_Core_DAO
::executequery($sql);
766 * Get timezone from CMS.
768 * @return string|false|null
770 public function getTimeZoneOffset() {
771 $timezone = $this->getTimeZoneString();
773 if ($timezone == 'UTC' ||
$timezone == 'Etc/UTC') {
774 // CRM-17072 Let's short-circuit all the zero handling & return it here!
777 $tzObj = new DateTimeZone($timezone);
778 $dateTime = new DateTime("now", $tzObj);
779 $tz = $tzObj->getOffset($dateTime);
790 $timeZoneOffset = sprintf("%02d:%02d", $tz / 3600, abs(($tz / 60) %
60));
792 if ($timeZoneOffset > 0) {
793 $timeZoneOffset = '+' . $timeZoneOffset;
795 return $timeZoneOffset;
801 * Get timezone as a string.
803 * Timezone string e.g. 'America/Los_Angeles'
805 public function getTimeZoneString() {
806 return date_default_timezone_get();
810 * Get Unique Identifier from UserFramework system (CMS).
812 * @param object $user
813 * Object as described by the User Framework.
816 * Unique identifier from the user Framework system
818 public function getUniqueIdentifierFromUserObject($user) {
823 * Get User ID from UserFramework system (CMS).
825 * @param object $user
827 * Object as described by the User Framework.
830 public function getUserIDFromUserObject($user) {
835 * Get an array of user details for a contact, containing at minimum the user ID & name.
837 * @param int $contactID
840 * CMS user details including
842 * - name (ie the system user name.
844 public function getUser($contactID) {
845 $ufMatch = civicrm_api3('UFMatch', 'getsingle', [
846 'contact_id' => $contactID,
847 'domain_id' => CRM_Core_Config
::domainID(),
850 'id' => $ufMatch['uf_id'],
851 'name' => $ufMatch['uf_name'],
856 * Get currently logged in user uf id.
859 * logged in user uf id.
861 public function getLoggedInUfID() {
866 * Get currently logged in user unique identifier - this tends to be the email address or user name.
868 * @return string|null
869 * logged in user unique identifier
871 public function getLoggedInUniqueIdentifier() {
876 * Return a UFID (user account ID from the UserFramework / CMS system.
878 * ID is based on the user object passed, defaulting to the logged in user if not passed.
880 * Note that ambiguous situation occurs in CRM_Core_BAO_UFMatch::synchronize - a cleaner approach would
881 * seem to be resolving the user id before calling the function.
883 * Note there is already a function getUFId which takes $username as a param - we could add $user
884 * as a second param to it but it seems messy - just overloading it because the name is taken.
886 * @param object $user
889 * User ID of UF System
891 public function getBestUFID($user = NULL) {
893 return $this->getUserIDFromUserObject($user);
895 return $this->getLoggedInUfID();
899 * Return a unique identifier (usually an email address or username) from the UserFramework / CMS system.
901 * This is based on the user object passed, defaulting to the logged in user if not passed.
903 * Note that ambiguous situation occurs in CRM_Core_BAO_UFMatch::synchronize - a cleaner approach would seem to be
904 * resolving the unique identifier before calling the function.
906 * @param object $user
909 * unique identifier from the UF System
911 public function getBestUFUniqueIdentifier($user = NULL) {
913 return $this->getUniqueIdentifierFromUserObject($user);
915 return $this->getLoggedInUniqueIdentifier();
919 * List modules installed in the CMS, including enabled and disabled ones.
924 public function getModules() {
929 * Get Url to view user record.
931 * @param int $contactID
934 * @return string|null
936 public function getUserRecordUrl($contactID) {
941 * Is the current user permitted to add a user.
945 public function checkPermissionAddUser() {
950 * Output code from error function.
952 * @param string $content
954 public function outputError($content) {
955 echo CRM_Utils_System
::theme($content);
961 * @param string $message
963 public function logger($message) {
967 * Append to coreResourcesList.
969 * @param \Civi\Core\Event\GenericHookEvent $e
971 public function appendCoreResources(\Civi\Core\Event\GenericHookEvent
$e) {
975 * Modify dynamic assets.
977 * @param \Civi\Core\Event\GenericHookEvent $e
979 public function alterAssetUrl(\Civi\Core\Event\GenericHookEvent
$e) {
983 * @param string $name
984 * @param string $value
986 public function setHttpHeader($name, $value) {
987 header("$name: $value");
991 * Create CRM contacts for all existing CMS users
996 public function synchronizeUsers() {
997 throw new Exception('CMS user creation not supported for this framework');
1002 * Send an HTTP Response base on PSR HTTP RespnseInterface response.
1004 * @param \Psr\Http\Message\ResponseInterface $response
1006 public function sendResponse(\Psr\Http\Message\ResponseInterface
$response) {
1007 http_response_code($response->getStatusCode());
1008 foreach ($response->getHeaders() as $name => $values) {
1009 CRM_Utils_System
::setHttpHeader($name, implode(', ', (array) $values));
1011 echo $response->getBody();
1012 CRM_Utils_System
::civiExit();
1016 * Start a new session.
1018 public function sessionStart() {
1023 * This exists because of https://www.drupal.org/node/3006306 where
1024 * they changed so that they don't start sessions for anonymous, but we
1027 public function getSessionId() {
1028 return session_id();
1034 * @return array|null
1036 public function getRoleNames() {
1041 * Determine if the Views module exists.
1045 public function viewsExists() {
1050 * Perform any necessary actions prior to redirecting via POST.
1052 public function prePostRedirect() {
1056 * Return the CMS-specific url for its permissions page
1059 public function getCMSPermissionsUrlParams() {
1064 * Get the CRM database as a 'prefix'.
1066 * This returns a string that can be prepended to a query to include a CRM table.
1068 * However, this string should contain backticks, or not, in accordance with the
1069 * CMS's drupal views expectations, if any.
1071 public function getCRMDatabasePrefix(): string {
1072 $crmDatabase = DB
::parseDSN(CRM_Core_Config
::singleton()->dsn
)['database'];
1073 $cmsDatabase = DB
::parseDSN(CRM_Core_Config
::singleton()->userFrameworkDSN
)['database'];
1074 if ($crmDatabase === $cmsDatabase) {
1077 return "`$crmDatabase`.";
1081 * Invalidates the cache of dynamic routes and forces a rebuild.
1083 public function invalidateRouteCache() {
1087 * Should the admin be able to set the password when creating a user
1088 * or does the CMS want it a different way.
1090 public function showPasswordFieldWhenAdminCreatesUser() {
1095 * Return the CMS-specific UF Group Types for profiles.
1098 public function getUfGroupTypes() {