Merge pull request #4906 from eileenmcnaughton/minor-tidies
[civicrm-core.git] / CRM / Utils / System / Drupal8.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.5 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2014 |
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 *
30 * @package CRM
31 * @copyright CiviCRM LLC (c) 2004-2014
32 * $Id$
33 *
34 */
35
36 /**
37 * Drupal specific stuff goes here
38 */
39 class CRM_Utils_System_Drupal8 extends CRM_Utils_System_DrupalBase {
40
41 /**
42 * Create a user in Drupal.
43 *
44 * @param array $params
45 * @param string $mail
46 * Email id for cms user.
47 *
48 * @return int|bool
49 * uid if user exists, false otherwise
50 */
51 public function createUser(&$params, $mail) {
52 $user = \Drupal::currentUser();
53 $user_register_conf = \Drupal::config('user.settings')->get('register');
54 $verify_mail_conf = \Drupal::config('user.settings')->get('verify_mail');
55
56 // Don't create user if we don't have permission to.
57 if (!$user->hasPermission('administer users') && $user_register_conf == 'admin_only') {
58 return FALSE;
59 }
60
61 $account = entity_create('user');
62 $account->setUsername($params['cms_name'])->setEmail($params[$mail]);
63
64 // Allow user to set password only if they are an admin or if
65 // the site settings don't require email verification.
66 if (!$verify_mail_conf || $user->hasPermission('administer users')) {
67 // @Todo: do we need to check that passwords match or assume this has already been done for us?
68 $account->setPassword($params['cms_pass']);
69 }
70
71 // Only activate account if we're admin or if anonymous users don't require
72 // approval to create accounts.
73 if ($user_register_conf != 'visitors' && !$user->hasPermission('administer users')) {
74 $account->block();
75 }
76
77 // Validate the user object
78 $violations = $account->validate();
79 if (count($violations)) {
80 return FALSE;
81 }
82
83 try {
84 $account->save();
85 }
86 catch (\Drupal\Core\Entity\EntityStorageException $e) {
87 return FALSE;
88 }
89
90 // Send off any emails as required.
91 // Possible values for $op:
92 // - 'register_admin_created': Welcome message for user created by the admin.
93 // - 'register_no_approval_required': Welcome message when user
94 // self-registers.
95 // - 'register_pending_approval': Welcome message, user pending admin
96 // approval.
97 // @Todo: Should we only send off emails if $params['notify'] is set?
98 switch (TRUE) {
99 case $user_register_conf == 'admin_only' || $user->isAuthenticated():
100 _user_mail_notify('register_admin_created', $account);
101 break;
102
103 case $user_register_conf == 'visitors':
104 _user_mail_notify('register_no_approval_required', $account);
105 break;
106
107 case 'visitors_admin_approval':
108 _user_mail_notify('register_pending_approval', $account);
109 break;
110 }
111
112 return $account->id();
113 }
114
115 /**
116 * Update the Drupal user's email address.
117 *
118 * @param int $ufID
119 * User ID in CMS.
120 * @param string $email
121 * Primary contact email address.
122 */
123 public function updateCMSName($ufID, $email) {
124 $user = user_load($ufID);
125 if ($user && $user->getEmail() != $email) {
126 $user->setEmail($email);
127
128 if (!count($user->validate())) {
129 $user->save();
130 }
131 }
132 }
133
134 /**
135 * Check if username and email exists in the drupal db
136 *
137 * @param array $params
138 * Array of name and mail values.
139 * @param array $errors
140 * Errors.
141 * @param string $emailName
142 * Field label for the 'email'.
143 *
144 *
145 * @return void
146 */
147 public static function checkUserNameEmailExists(&$params, &$errors, $emailName = 'email') {
148 // If we are given a name, let's check to see if it already exists.
149 if (!empty($params['name'])) {
150 $name = $params['name'];
151
152 $user = entity_create('user');
153 $user->setUsername($name);
154
155 // This checks for both username uniqueness and validity.
156 $violations = iterator_to_array($user->validate());
157 // We only care about violations on the username field; discard the rest.
158 $violations = array_filter($violations, function ($v) {
159 return $v->getPropertyPath() == 'name.0.value';
160 });
161 if (count($violations) > 0) {
162 $errors['cms_name'] = $violations[0]->getMessage();
163 }
164 }
165
166 // And if we are given an email address, let's check to see if it already exists.
167 if (!empty($params[$emailName])) {
168 $mail = $params[$emailName];
169
170 $user = entity_create('user');
171 $user->setEmail($mail);
172
173 // This checks for both email uniqueness.
174 $violations = iterator_to_array($user->validate());
175 // We only care about violations on the email field; discard the rest.
176 $violations = array_filter($violations, function ($v) {
177 return $v->getPropertyPath() == 'mail.0.value';
178 });
179 if (count($violations) > 0) {
180 $errors[$emailName] = $violations[0]->getMessage();
181 }
182 }
183 }
184
185 /**
186 * Get the drupal destination string. When this is passed in the
187 * URL the user will be directed to it after filling in the drupal form
188 *
189 * @param CRM_Core_Form $form
190 * Form object representing the 'current' form - to which the user will be returned.
191 * @return string
192 * destination value for URL
193 */
194 public function getLoginDestination(&$form) {
195 $args = NULL;
196
197 $id = $form->get('id');
198 if ($id) {
199 $args .= "&id=$id";
200 }
201 else {
202 $gid = $form->get('gid');
203 if ($gid) {
204 $args .= "&gid=$gid";
205 }
206 else {
207 // Setup Personal Campaign Page link uses pageId
208 $pageId = $form->get('pageId');
209 if ($pageId) {
210 $component = $form->get('component');
211 $args .= "&pageId=$pageId&component=$component&action=add";
212 }
213 }
214 }
215
216 $destination = NULL;
217 if ($args) {
218 // append destination so user is returned to form they came from after login
219 $destination = CRM_Utils_System::currentPath() . '?reset=1' . $args;
220 }
221 return $destination;
222 }
223
224 /**
225 * Get user login URL for hosting CMS (method declared in each CMS system class)
226 *
227 * @param string $destination
228 * If present, add destination to querystring (works for Drupal only).
229 *
230 * @return string
231 * loginURL for the current CMS
232 * @static
233 */
234 public function getLoginURL($destination = '') {
235 $query = $destination ? array('destination' => $destination) : array();
236 return \Drupal::url('user.page', array(), array('query' => $query));
237 }
238
239
240 /**
241 * Sets the title of the page
242 *
243 * @param string $title
244 * @param string $pageTitle
245 *
246 * @return void
247 */
248 public function setTitle($title, $pageTitle = NULL) {
249 if (!$pageTitle) {
250 $pageTitle = $title;
251 }
252
253 \Drupal::service('civicrm.page_state')->setTitle($pageTitle);
254 }
255
256 /**
257 * Append an additional breadcrumb tag to the existing breadcrumb
258 *
259 * @param $breadcrumbs
260 *
261 * @internal param string $title
262 * @internal param string $url
263 *
264 * @return void
265 */
266 public function appendBreadCrumb($breadcrumbs) {
267 $civicrmPageState = \Drupal::service('civicrm.page_state');
268 foreach ($breadcrumbs as $breadcrumb) {
269 $civicrmPageState->addBreadcrumb($breadcrumb['title'], $breadcrumb['url']);
270 }
271 }
272
273 /**
274 * Reset an additional breadcrumb tag to the existing breadcrumb
275 *
276 * @return void
277 */
278 public function resetBreadCrumb() {
279 \Drupal::service('civicrm.page_state')->resetBreadcrumbs();
280 }
281
282 /**
283 * Append a string to the head of the html file
284 *
285 * @param string $header
286 * The new string to be appended.
287 *
288 * @return void
289 */
290 public function addHTMLHead($header) {
291 \Drupal::service('civicrm.page_state')->addHtmlHeader($header);
292 }
293
294 /**
295 * Add a script file
296 *
297 * @param $url : string, absolute path to file
298 * @param string $region
299 * location within the document: 'html-header', 'page-header', 'page-footer'.
300 *
301 * Note: This function is not to be called directly
302 * @see CRM_Core_Region::render()
303 *
304 * @return bool
305 * TRUE if we support this operation in this CMS, FALSE otherwise
306 */
307 public function addScriptUrl($url, $region) {
308 $options = array('group' => JS_LIBRARY, 'weight' => 10);
309 switch ($region) {
310 case 'html-header':
311 case 'page-footer':
312 $options['scope'] = substr($region, 5);
313 break;
314
315 default:
316 return FALSE;
317 }
318 // If the path is within the drupal directory we can use the more efficient 'file' setting
319 $options['type'] = $this->formatResourceUrl($url) ? 'file' : 'external';
320 \Drupal::service('civicrm.page_state')->addJS($url, $options);
321 return TRUE;
322 }
323
324 /**
325 * Add an inline script
326 *
327 * @param $code : string, javascript code
328 * @param string $region
329 * location within the document: 'html-header', 'page-header', 'page-footer'.
330 *
331 * Note: This function is not to be called directly
332 * @see CRM_Core_Region::render()
333 *
334 * @return bool
335 * TRUE if we support this operation in this CMS, FALSE otherwise
336 */
337 public function addScript($code, $region) {
338 $options = array('type' => 'inline', 'group' => JS_LIBRARY, 'weight' => 10);
339 switch ($region) {
340 case 'html-header':
341 case 'page-footer':
342 $options['scope'] = substr($region, 5);
343 break;
344
345 default:
346 return FALSE;
347 }
348 \Drupal::service('civicrm.page_state')->addJS($code, $options);
349 return TRUE;
350 }
351
352 /**
353 * Add a css file
354 *
355 * @param $url : string, absolute path to file
356 * @param string $region
357 * location within the document: 'html-header', 'page-header', 'page-footer'.
358 *
359 * Note: This function is not to be called directly
360 * @see CRM_Core_Region::render()
361 *
362 * @return bool
363 * TRUE if we support this operation in this CMS, FALSE otherwise
364 */
365 public function addStyleUrl($url, $region) {
366 if ($region != 'html-header') {
367 return FALSE;
368 }
369 $options = array();
370 // If the path is within the drupal directory we can use the more efficient 'file' setting
371 $options['type'] = $this->formatResourceUrl($url) ? 'file' : 'external';
372 \Drupal::service('civicrm.page_state')->addCSS($url, $options);
373 return TRUE;
374 }
375
376 /**
377 * Add an inline style
378 *
379 * @param $code : string, css code
380 * @param string $region
381 * location within the document: 'html-header', 'page-header', 'page-footer'.
382 *
383 * Note: This function is not to be called directly
384 * @see CRM_Core_Region::render()
385 *
386 * @return bool
387 * TRUE if we support this operation in this CMS, FALSE otherwise
388 */
389 public function addStyle($code, $region) {
390 if ($region != 'html-header') {
391 return FALSE;
392 }
393 $options = array('type' => 'inline');
394 \Drupal::service('civicrm.page_state')->addCSS($code, $options);
395 return TRUE;
396 }
397
398 /**
399 * Check if a resource url is within the drupal directory and format appropriately
400 *
401 * This seems to be a legacy function. We assume all resources are within the drupal
402 * directory and always return TRUE. As well, we clean up the $url.
403 *
404 * @param $url
405 *
406 * @return bool
407 */
408 public function formatResourceUrl(&$url) {
409 // Remove leading slash if present.
410 $url = ltrim($url, '/');
411
412 // Remove query string — presumably added to stop intermediary caching.
413 if (($pos = strpos($url, '?')) !== FALSE) {
414 $url = substr($url, 0, $pos);
415 }
416
417 return TRUE;
418 }
419
420 /**
421 * Rewrite various system urls to https
422 *
423 * This function does nothing in Drupal 8. Changes to the base_url should be made
424 * in settings.php directly.
425 *
426 * @param null
427 *
428 * @return void
429 */
430 public function mapConfigToSSL() {
431 }
432
433 /**
434 * @param string $path
435 * The base path (eg. civicrm/search/contact).
436 * @param string $query
437 * The query string (eg. reset=1&cid=66) but html encoded(?) (optional).
438 * @param bool $absolute
439 * Produce an absolute including domain and protocol (optional).
440 * @param string $fragment
441 * A named anchor (optional).
442 * @param bool $htmlize
443 * Produce a html encoded url (optional).
444 * @param bool $frontend
445 * A joomla hack (unused).
446 * @param bool $forceBackend
447 * A joomla jack (unused).
448 * @return string
449 */
450 public function url($path = '', $query = '', $absolute = FALSE, $fragment = '', $htmlize = FALSE, $frontend = FALSE, $forceBackend = FALSE) {
451 $query = html_entity_decode($query);
452 $url = \Drupal\civicrm\CivicrmHelper::parseURL("{$path}?{$query}");
453
454 try {
455 $url = \Drupal::url($url['route_name'], array(), array(
456 'query' => $url['query'],
457 'absolute' => $absolute,
458 'fragment' => $fragment,
459 ));
460 }
461 catch (Exception $e) {
462 $url = '';
463 }
464
465 if ($htmlize) {
466 $url = htmlentities($url);
467 }
468 return $url;
469 }
470
471
472 /**
473 * Authenticate the user against the drupal db
474 *
475 * @param string $name
476 * The user name.
477 * @param string $password
478 * The password for the above user name.
479 * @param bool $loadCMSBootstrap
480 * Load cms bootstrap?.
481 * @param NULL|string $realPath filename of script
482 *
483 * @return array|bool
484 * [contactID, ufID, uniqueString] if success else false if no auth
485 *
486 * This always bootstraps Drupal
487 */
488 public function authenticate($name, $password, $loadCMSBootstrap = FALSE, $realPath = NULL) {
489 (new CRM_Utils_System_Drupal8())->loadBootStrap(array(), FALSE);
490
491 $uid = \Drupal::service('user.auth')->authenticate($name, $password);
492 $contact_id = CRM_Core_BAO_UFMatch::getContactId($uid);
493
494 return array($contact_id, $uid, mt_rand());
495 }
496
497 /**
498 * Load user into session
499 */
500 public function loadUser($username) {
501 $user = user_load_by_name($username);
502 if (!$user) {
503 return FALSE;
504 }
505
506 // Set Drupal's current user to the loaded user.
507 \Drupal::currentUser()->setAccount($user);
508
509 $uid = $user->id();
510 $contact_id = CRM_Core_BAO_UFMatch::getContactId($uid);
511
512 // Store the contact id and user id in the session
513 $session = CRM_Core_Session::singleton();
514 $session->set('ufID', $uid);
515 $session->set('userID', $contact_id);
516 return TRUE;
517 }
518
519 /**
520 * Determine the native ID of the CMS user
521 *
522 * @param string $username
523 * @return int|NULL
524 */
525 public function getUfId($username) {
526 if ($id = user_load_by_name($username)->id()) {
527 return $id;
528 }
529 }
530
531 /**
532 * Set a message in the UF to display to a user
533 *
534 * @param string $message
535 * The message to set.
536 */
537 public function setMessage($message) {
538 drupal_set_message($message);
539 }
540
541 public function permissionDenied() {
542 \Drupal::service('civicrm.page_state')->setAccessDenied();
543 }
544
545 /**
546 * In previous versions, this function was the controller for logging out. In Drupal 8, we rewrite the route
547 * to hand off logout to the standard Drupal logout controller. This function should therefore never be called.
548 */
549 public function logout() {
550 // Pass
551 }
552
553 /**
554 * Load drupal bootstrap
555 *
556 * @param array $params
557 * Either uid, or name & pass.
558 * @param bool $loadUser
559 * Boolean Require CMS user load.
560 * @param bool $throwError
561 * If true, print error on failure and exit.
562 * @param bool|string $realPath path to script
563 *
564 * @return bool
565 * @Todo Handle setting cleanurls configuration for CiviCRM?
566 */
567 public function loadBootStrap($params = array(), $loadUser = TRUE, $throwError = TRUE, $realPath = NULL) {
568 static $run_once = FALSE;
569 if ($run_once) {
570 return TRUE;
571 }
572 else {
573 $run_once = TRUE;
574 }
575
576 if (!($root = $this->cmsRootPath())) {
577 return FALSE;
578 }
579 chdir($root);
580
581 // Create a mock $request object
582 $autoloader = require_once $root . '/core/vendor/autoload.php';
583 // @Todo: do we need to handle case where $_SERVER has no HTTP_HOST key, ie. when run via cli?
584 $request = new \Symfony\Component\HttpFoundation\Request(array(), array(), array(), array(), array(), $_SERVER);
585
586 // Create a kernel and boot it.
587 \Drupal\Core\DrupalKernel::createFromRequest($request, $autoloader, 'prod')->prepareLegacyRequest($request);
588
589 // Initialize Civicrm
590 \Drupal::service('civicrm');
591
592 // We need to call the config hook again, since we now know
593 // all the modules that are listening on it (CRM-8655).
594 CRM_Utils_Hook::config($config);
595
596 if ($loadUser) {
597 if (!empty($params['uid']) && $username = \Drupal\user\Entity\User::load($uid)->getUsername()) {
598 $this->loadUser($username);
599 }
600 elseif (!empty($params['name']) && !empty($params['pass']) && $this->authenticate($params['name'], $params['pass'])) {
601 $this->loadUser($params['name']);
602 }
603 }
604 return TRUE;
605 }
606
607 /**
608 * Determine the location of the CMS root.
609 * @param null $path
610 *
611 * @return NULL|string
612 */
613 public function cmsRootPath($path = NULL) {
614 if (defined('DRUPAL_ROOT')) {
615 return DRUPAL_ROOT;
616 }
617
618 // It looks like Drupal hasn't been bootstrapped.
619 // We're going to attempt to discover the root Drupal path
620 // by climbing out of the folder hierarchy and looking around to see
621 // if we've found the Drupal root directory.
622 if (!$path) {
623 $path = $_SERVER['SCRIPT_FILENAME'];
624 }
625
626 // Normalize and explode path into its component paths.
627 $paths = explode(DIRECTORY_SEPARATOR, realpath($path));
628
629 // Remove script filename from array of directories.
630 array_pop($paths);
631
632 while (count($paths)) {
633 $candidate = implode('/', $paths);
634 if (file_exists($candidate . "/core/includes/bootstrap.inc")) {
635 return $candidate;
636 }
637
638 array_pop($paths);
639 }
640 }
641
642 /**
643 * Check if user is logged in.
644 *
645 * @return bool
646 */
647 public function isUserLoggedIn() {
648 return \Drupal::currentUser()->isAuthenticated();
649 }
650
651 /**
652 * Get currently logged in user uf id.
653 *
654 * @return int
655 * $userID logged in user uf id.
656 */
657 public function getLoggedInUfID() {
658 if ($id = \Drupal::currentUser()->id()) {
659 return $id;
660 }
661 }
662
663 /**
664 * Get the default location for CiviCRM blocks
665 *
666 * @return string
667 */
668 public function getDefaultBlockLocation() {
669 return 'sidebar_first';
670 }
671 }