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