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