From 7e9cadcf6aa42dd7d2a2a0ae40282075142766fc Mon Sep 17 00:00:00 2001 From: Eileen McNaughton Date: Wed, 1 Oct 2014 11:40:23 +1300 Subject: [PATCH] CRM-15297 add Drupal8 functions from GSOC to Drupal8 class where the d8 function is unchanged from d7 put in the DrupalBase class instead --- CRM/Utils/System/Drupal8.php | 587 +++++++++++++++++++++++++++++++- CRM/Utils/System/DrupalBase.php | 190 +++++++++++ 2 files changed, 769 insertions(+), 8 deletions(-) diff --git a/CRM/Utils/System/Drupal8.php b/CRM/Utils/System/Drupal8.php index 461a6946e6..57d3f919f2 100644 --- a/CRM/Utils/System/Drupal8.php +++ b/CRM/Utils/System/Drupal8.php @@ -38,6 +38,178 @@ */ class CRM_Utils_System_Drupal8 extends CRM_Utils_System_DrupalBase { + /** + * Function to create a user in Drupal. + * + * @param array $params associated array + * @param string $mail email id for cms user + * + * @return uid if user exists, false otherwise + * + * @access public + * + */ + function createUser(&$params, $mail) { + $user = \Drupal::currentUser(); + $user_register_conf = \Drupal::config('user.settings')->get('register'); + $verify_mail_conf = \Drupal::config('user.settings')->get('verify_mail'); + + // Don't create user if we don't have permission to. + if (!$user->hasPermission('administer users') && $user_register_conf == 'admin_only') { + return FALSE; + } + + $account = entity_create('user'); + $account->setUsername($params['cms_name'])->setEmail($params[$mail]); + + // Allow user to set password only if they are an admin or if + // the site settings don't require email verification. + if (!$verify_mail_conf || $user->hasPermission('administer users')) { + // @Todo: do we need to check that passwords match or assume this has already been done for us? + $account->setPassword($params['cms_pass']); + } + + // Only activate account if we're admin or if anonymous users don't require + // approval to create accounts. + if ($user_register_conf != 'visitors' && !$user->hasPermission('administer users')) { + $account->block(); + } + + // Validate the user object + $violations = $account->validate(); + if (count($violations)) { + return FALSE; + } + + try { + $account->save(); + } + catch (\Drupal\Core\Entity\EntityStorageException $e) { + return FALSE; + } + + // Send off any emails as required. + // Possible values for $op: + // - 'register_admin_created': Welcome message for user created by the admin. + // - 'register_no_approval_required': Welcome message when user + // self-registers. + // - 'register_pending_approval': Welcome message, user pending admin + // approval. + // @Todo: Should we only send off emails if $params['notify'] is set? + switch (TRUE) { + case $user_register_conf == 'admin_only' || $user->isAuthenticated(): + _user_mail_notify('register_admin_created', $account); + break; + case $user_register_conf == 'visitors': + _user_mail_notify('register_no_approval_required', $account); + break; + case 'visitors_admin_approval': + _user_mail_notify('register_pending_approval', $account); + break; + } + + return $account->id(); + } + + /** + * Update the Drupal user's email address. + * + * @param integer $ufID User ID in CMS + * @param string $email Primary contact email address + */ + function updateCMSName($ufID, $email) { + $user = user_load($ufID); + if ($user && $user->getEmail() != $email) { + $user->setEmail($email); + + if (!count($user->validate())) { + $user->save(); + } + } + } + + /** + * Check if username and email exists in the drupal db + * + * @param array $params array of name and mail values + * @param array $errors errors + * @param string $emailName field label for the 'email' + * + * + * @return void + */ + static function checkUserNameEmailExists(&$params, &$errors, $emailName = 'email') { + // If we are given a name, let's check to see if it already exists. + if (!empty($params['name'])) { + $name = $params['name']; + + $user = entity_create('user'); + $user->setUsername($name); + + // This checks for both username uniqueness and validity. + $violations = iterator_to_array($user->validate()); + // We only care about violations on the username field; discard the rest. + $violations = array_filter($violations, function ($v) { return $v->getPropertyPath() == 'name.0.value'; }); + if (count($violations) > 0) { + $errors['cms_name'] = $violations[0]->getMessage(); + } + } + + // And if we are given an email address, let's check to see if it already exists. + if (!empty($params[$emailName])) { + $mail = $params[$emailName]; + + $user = entity_create('user'); + $user->setEmail($mail); + + // This checks for both email uniqueness. + $violations = iterator_to_array($user->validate()); + // We only care about violations on the email field; discard the rest. + $violations = array_filter($violations, function ($v) { return $v->getPropertyPath() == 'mail.0.value'; }); + if (count($violations) > 0) { + $errors[$emailName] = $violations[0]->getMessage(); + } + } + } + + /** + * Function to get the drupal destination string. When this is passed in the + * URL the user will be directed to it after filling in the drupal form + * + * @param object $form Form object representing the 'current' form - to which the user will be returned + * @return string $destination destination value for URL + * + */ + function getLoginDestination(&$form) { + $args = NULL; + + $id = $form->get('id'); + if ($id) { + $args .= "&id=$id"; + } + else { + $gid = $form->get('gid'); + if ($gid) { + $args .= "&gid=$gid"; + } + else { + // Setup Personal Campaign Page link uses pageId + $pageId = $form->get('pageId'); + if ($pageId) { + $component = $form->get('component'); + $args .= "&pageId=$pageId&component=$component&action=add"; + } + } + } + + $destination = NULL; + if ($args) { + // append destination so user is returned to form they came from after login + $destination = CRM_Utils_System::currentPath() . '?reset=1' . $args; + } + return $destination; + } + /** * Get user login URL for hosting CMS (method declared in each CMS system class) * @@ -47,14 +219,413 @@ class CRM_Utils_System_Drupal8 extends CRM_Utils_System_DrupalBase { * @static */ public function getLoginURL($destination = '') { - $loginURL = CRM_Core_Config::singleton()->userFrameworkBaseURL; - //@todo this is not the right way to get the url in d8 - $loginURL .= 'user'; - if (!empty($destination)) { - // append destination so user is returned to form they came from after login - $loginURL .= '?destination=' . urlencode($destination); + $query = $destination ? array('destination' => $destination) : array(); + return \Drupal::url('user.page', array(), array('query' => $query)); + } + + + /** + * sets the title of the page + * + * @param string $title + * @param string $pageTitle + * + * @return void + * @access public + */ + function setTitle($title, $pageTitle = NULL) { + if (!$pageTitle) { + $pageTitle = $title; + } + + \Drupal::service('civicrm.page_state')->setTitle($pageTitle); + } + + /** + * Append an additional breadcrumb tag to the existing breadcrumb + * + * @param $breadcrumbs + * + * @internal param string $title + * @internal param string $url + * + * @return void + * @access public + */ + function appendBreadCrumb($breadcrumbs) { + $civicrmPageState = \Drupal::service('civicrm.page_state'); + foreach ($breadcrumbs as $breadcrumb) { + $civicrmPageState->addBreadcrumb($breadcrumb['title'], $breadcrumb['url']); + } + } + + /** + * Reset an additional breadcrumb tag to the existing breadcrumb + * + * @return void + * @access public + */ + function resetBreadCrumb() { + \Drupal::service('civicrm.page_state')->resetBreadcrumbs(); + } + + /** + * Append a string to the head of the html file + * + * @param string $header the new string to be appended + * + * @return void + * @access public + */ + function addHTMLHead($header) { + \Drupal::service('civicrm.page_state')->addHtmlHeader($header); + } + + /** + * Add a script file + * + * @param $url: string, absolute path to file + * @param $region string, location within the document: 'html-header', 'page-header', 'page-footer' + * + * Note: This function is not to be called directly + * @see CRM_Core_Region::render() + * + * @return bool TRUE if we support this operation in this CMS, FALSE otherwise + * @access public + */ + public function addScriptUrl($url, $region) { + $options = array('group' => JS_LIBRARY, 'weight' => 10); + switch ($region) { + case 'html-header': + case 'page-footer': + $options['scope'] = substr($region, 5); + break; + default: + return FALSE; + } + // If the path is within the drupal directory we can use the more efficient 'file' setting + $options['type'] = $this->formatResourceUrl($url) ? 'file' : 'external'; + \Drupal::service('civicrm.page_state')->addJS($url, $options); + return TRUE; + } + + /** + * Add an inline script + * + * @param $code: string, javascript code + * @param $region string, location within the document: 'html-header', 'page-header', 'page-footer' + * + * Note: This function is not to be called directly + * @see CRM_Core_Region::render() + * + * @return bool TRUE if we support this operation in this CMS, FALSE otherwise + * @access public + */ + public function addScript($code, $region) { + $options = array('type' => 'inline', 'group' => JS_LIBRARY, 'weight' => 10); + switch ($region) { + case 'html-header': + case 'page-footer': + $options['scope'] = substr($region, 5); + break; + default: + return FALSE; + } + \Drupal::service('civicrm.page_state')->addJS($code, $options); + return TRUE; + } + + /** + * Add a css file + * + * @param $url: string, absolute path to file + * @param $region string, location within the document: 'html-header', 'page-header', 'page-footer' + * + * Note: This function is not to be called directly + * @see CRM_Core_Region::render() + * + * @return bool TRUE if we support this operation in this CMS, FALSE otherwise + * @access public + */ + public function addStyleUrl($url, $region) { + if ($region != 'html-header') { + return FALSE; } - return $loginURL; + $options = array(); + // If the path is within the drupal directory we can use the more efficient 'file' setting + $options['type'] = $this->formatResourceUrl($url) ? 'file' : 'external'; + \Drupal::service('civicrm.page_state')->addCSS($url, $options); + return TRUE; } -} \ No newline at end of file + /** + * Add an inline style + * + * @param $code: string, css code + * @param $region string, location within the document: 'html-header', 'page-header', 'page-footer' + * + * Note: This function is not to be called directly + * @see CRM_Core_Region::render() + * + * @return bool TRUE if we support this operation in this CMS, FALSE otherwise + * @access public + */ + public function addStyle($code, $region) { + if ($region != 'html-header') { + return FALSE; + } + $options = array('type' => 'inline'); + \Drupal::service('civicrm.page_state')->addCSS($code, $options); + return TRUE; + } + + /** + * Check if a resource url is within the drupal directory and format appropriately + * + * This seems to be a legacy function. We assume all resources are within the drupal + * directory and always return TRUE. As well, we clean up the $url. + * + * @param $url + * + * @return bool + */ + function formatResourceUrl(&$url) { + // Remove leading slash if present. + $url = ltrim($url, '/'); + + // Remove query string — presumably added to stop intermediary caching. + if (($pos = strpos($url, '?')) !== FALSE) { + $url = substr($url, 0, $pos); + } + + return TRUE; + } + + /** + * Rewrite various system urls to https + * + * This function does nothing in Drupal 8. Changes to the base_url should be made + * in settings.php directly. + * + * @param null + * + * @return void + * @access public + */ + function mapConfigToSSL() { + } + + /** + * @param string $path The base path (eg. civicrm/search/contact) + * @param string $query The query string (eg. reset=1&cid=66) but html encoded(?) (optional) + * @param bool $absolute Produce an absolute including domain and protocol (optional) + * @param string $fragment A named anchor (optional) + * @param bool $htmlize Produce a html encoded url (optional) + * @param bool $frontend A joomla hack (unused) + * @param bool $forceBackend A joomla jack (unused) + * @return string + */ + function url($path = '', $query = '', $absolute = FALSE, $fragment = '', $htmlize = FALSE, $frontend = FALSE, $forceBackend = FALSE) { + $query = html_entity_decode($query); + $url = \Drupal\civicrm\CivicrmHelper::parseURL("{$path}?{$query}"); + + try { + $url = \Drupal::url($url['route_name'], array(), array( + 'query' => $url['query'], + 'absolute' => $absolute, + 'fragment' => $fragment, + )); + } + catch (Exception $e) { + $url = ''; + } + + if ($htmlize) { + $url = htmlentities($url); + } + return $url; + } + + + /** + * Authenticate the user against the drupal db + * + * @param string $name the user name + * @param string $password the password for the above user name + * @param boolean $loadCMSBootstrap load cms bootstrap? + * @param NULL|string $realPath filename of script + * + * @return mixed false if no auth + * array( + * contactID, ufID, unique string ) if success + * @access public + * + * This always bootstraps Drupal + */ + function authenticate($name, $password, $loadCMSBootstrap = FALSE, $realPath = NULL) { + (new CRM_Utils_System_Drupal8())->loadBootStrap(array(), FALSE); + + $uid = \Drupal::service('user.auth')->authenticate($name, $password); + $contact_id = CRM_Core_BAO_UFMatch::getContactId($uid); + + return array($contact_id, $uid, mt_rand()); + } + + /** + * Load user into session + */ + function loadUser($username) { + $user = user_load_by_name($username); + if (!$user) { + return FALSE; + } + + // Set Drupal's current user to the loaded user. + \Drupal::currentUser()->setAccount($user); + + $uid = $user->id(); + $contact_id = CRM_Core_BAO_UFMatch::getContactId($uid); + + // Store the contact id and user id in the session + $session = CRM_Core_Session::singleton(); + $session->set('ufID', $uid); + $session->set('userID', $contact_id); + return TRUE; + } + + /** + * Determine the native ID of the CMS user + * + * @param $username + * @return int|NULL + */ + function getUfId($username) { + if ($id = user_load_by_name($username)->id()) { + return $id; + } + } + + /** + * Set a message in the UF to display to a user + * + * @param string $message the message to set + * + * @access public + */ + function setMessage($message) { + drupal_set_message($message); + } + + function permissionDenied() { + \Drupal::service('civicrm.page_state')->setAccessDenied(); + } + + /** + * In previous versions, this function was the controller for logging out. In Drupal 8, we rewrite the route + * to hand off logout to the standard Drupal logout controller. This function should therefore never be called. + */ + function logout() { + // Pass + } + + /** + * load drupal bootstrap + * + * @param array $params Either uid, or name & pass. + * @param boolean $loadUser boolean Require CMS user load. + * @param boolean $throwError If true, print error on failure and exit. + * @param boolean|string $realPath path to script + * + * @return bool + * @Todo Handle setting cleanurls configuration for CiviCRM? + */ + function loadBootStrap($params = array(), $loadUser = TRUE, $throwError = TRUE, $realPath = NULL) { + static $run_once = FALSE; + if ($run_once) return TRUE; else $run_once = TRUE; + + if (!($root = $this->cmsRootPath())) { + return FALSE; + } + chdir($root); + + // Create a mock $request object + $autoloader = require_once $root . '/core/vendor/autoload.php'; + // @Todo: do we need to handle case where $_SERVER has no HTTP_HOST key, ie. when run via cli? + $request = new \Symfony\Component\HttpFoundation\Request(array(), array(), array(), array(), array(), $_SERVER); + + // Create a kernel and boot it. + \Drupal\Core\DrupalKernel::createFromRequest($request, $autoloader, 'prod')->prepareLegacyRequest($request); + + // Initialize Civicrm + \Drupal::service('civicrm'); + + // We need to call the config hook again, since we now know + // all the modules that are listening on it (CRM-8655). + CRM_Utils_Hook::config($config); + + if ($loadUser) { + if (!empty($params['uid']) && $username = \Drupal\user\Entity\User::load($uid)->getUsername()) { + $this->loadUser($username); + } + elseif (!empty($params['name']) && !empty($params['pass']) && $this->authenticate($params['name'], $params['pass'])) { + $this->loadUser($params['name']); + } + } + return TRUE; + } + + /** + * Determine the location of the CMS root. + * @param null $path + * + * @return NULL|string + */ + function cmsRootPath($path = NULL) { + if (defined('DRUPAL_ROOT')) { + return DRUPAL_ROOT; + } + + // It looks like Drupal hasn't been bootstrapped. + // We're going to attempt to discover the root Drupal path + // by climbing out of the folder hierarchy and looking around to see + // if we've found the Drupal root directory. + if (!$path) { + $path = $_SERVER['SCRIPT_FILENAME']; + } + + // Normalize and explode path into its component paths. + $paths = explode(DIRECTORY_SEPARATOR, realpath($path)); + + // Remove script filename from array of directories. + array_pop($paths); + + while (count($paths)) { + $candidate = implode('/', $paths); + if (file_exists($candidate . "/core/includes/bootstrap.inc")) { + return $candidate; + } + + array_pop($paths); + } + } + + /** + * Check if user is logged in. + * + * @return bool + */ + public function isUserLoggedIn() { + return \Drupal::currentUser()->isAuthenticated(); + } + + /** + * Get currently logged in user uf id. + * + * @return int $userID logged in user uf id. + */ + public function getLoggedInUfID() { + if ($id = \Drupal::currentUser()->id()) { + return $id; + } + } +} diff --git a/CRM/Utils/System/DrupalBase.php b/CRM/Utils/System/DrupalBase.php index 0e63845ee1..71f154d015 100644 --- a/CRM/Utils/System/DrupalBase.php +++ b/CRM/Utils/System/DrupalBase.php @@ -297,4 +297,194 @@ abstract class CRM_Utils_System_DrupalBase extends CRM_Utils_System_Base { function appendCoreResources(&$list) { $list[] = 'js/crm.drupal.js'; } + + /** + * Reset any system caches that may be required for proper CiviCRM + * integration. + */ + function flush() { + drupal_flush_all_caches(); + } + + /** + * Get a list of all installed modules, including enabled and disabled ones + * + * @return array CRM_Core_Module + * + */ + function getModules() { + $result = array(); + $q = db_query('SELECT name, status FROM {system} WHERE type = \'module\' AND schema_version <> -1'); + foreach ($q as $row) { + $result[] = new CRM_Core_Module('drupal.' . $row->name, ($row->status == 1) ? TRUE : FALSE); + } + return $result; + } + + /** + * Find any users/roles/security-principals with the given permission + * and replace it with one or more permissions. + * + * @param $oldPerm string + * @param $newPerms array, strings + * + * @return void + */ + function replacePermission($oldPerm, $newPerms) { + $roles = user_roles(FALSE, $oldPerm); + if (!empty($roles)) { + foreach (array_keys($roles) as $rid) { + user_role_revoke_permissions($rid, array($oldPerm)); + user_role_grant_permissions($rid, $newPerms); + } + } + } + /** + * Format the url as per language Negotiation. + * + * @param string $url + * + * @return string $url, formatted url. + * @static + */ + function languageNegotiationURL($url, $addLanguagePart = TRUE, $removeLanguagePart = FALSE) { + if (empty($url)) { + return $url; + } + + //CRM-7803 -from d7 onward. + $config = CRM_Core_Config::singleton(); + if (function_exists('variable_get') && + module_exists('locale') && + function_exists('language_negotiation_get') + ) { + global $language; + + //does user configuration allow language + //support from the URL (Path prefix or domain) + if (language_negotiation_get('language') == 'locale-url') { + $urlType = variable_get('locale_language_negotiation_url_part'); + + //url prefix + if ($urlType == LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX) { + if (isset($language->prefix) && $language->prefix) { + if ($addLanguagePart) { + $url .= $language->prefix . '/'; + } + if ($removeLanguagePart) { + $url = str_replace("/{$language->prefix}/", '/', $url); + } + } + } + //domain + if ($urlType == LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN) { + if (isset($language->domain) && $language->domain) { + if ($addLanguagePart) { + $url = (CRM_Utils_System::isSSL() ? 'https' : 'http') . '://' . $language->domain . base_path(); + } + if ($removeLanguagePart && defined('CIVICRM_UF_BASEURL')) { + $url = str_replace('\\', '/', $url); + $parseUrl = parse_url($url); + + //kinda hackish but not sure how to do it right + //hope http_build_url() will help at some point. + if (is_array($parseUrl) && !empty($parseUrl)) { + $urlParts = explode('/', $url); + $hostKey = array_search($parseUrl['host'], $urlParts); + $ufUrlParts = parse_url(CIVICRM_UF_BASEURL); + $urlParts[$hostKey] = $ufUrlParts['host']; + $url = implode('/', $urlParts); + } + } + } + } + } + } + return $url; + } + + /** + * GET CMS Version + * @return string + */ + function getVersion() { + return defined('VERSION') ? VERSION : 'Unknown'; + } + + /** + */ + function updateCategories() { + // copied this from profile.module. Seems a bit inefficient, but i dont know a better way + // CRM-3600 + cache_clear_all(); + menu_rebuild(); + } + + /** + * Get the default location for CiviCRM blocks + * + * @return string + * + */ + function getDefaultBlockLocation() { + return 'sidebar_first'; + } + /** + * Get the locale set in the hosting CMS + * + * @return string with the locale or null for none + * + */ + function getUFLocale() { + // return CiviCRM’s xx_YY locale that either matches Drupal’s Chinese locale + // (for CRM-6281), Drupal’s xx_YY or is retrieved based on Drupal’s xx + // sometimes for CLI based on order called, this might not be set and/or empty + global $language; + + if (empty($language)) { + return NULL; + } + + if ($language->language == 'zh-hans') { + return 'zh_CN'; + } + + if ($language->language == 'zh-hant') { + return 'zh_TW'; + } + + if (preg_match('/^.._..$/', $language->language)) { + return $language->language; + } + + return CRM_Core_I18n_PseudoConstant::longForShort(substr($language->language, 0, 2)); + } + /** + * Perform any post login activities required by the UF - + * e.g. for drupal: records a watchdog message about the new session, saves the login timestamp, + * calls hook_user op 'login' and generates a new session. + * + * @param array params + * + * FIXME: Document values accepted/required by $params + * + */ + function userLoginFinalize($params = array()){ + user_login_finalize($params); + } + + /** + * figure out the post url for the form + * + * @param mix $action the default action if one is pre-specified + * + * @return string the url to post the form + * @access public + */ + function postURL($action) { + if (!empty($action)) { + return $action; + } + return $this->url($_GET['q']); + } } -- 2.25.1