b4b725ef3ef4681eb0127955189ca7b663689a46
[civicrm-core.git] / CRM / Utils / System / Drupal8.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.7 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2017 |
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-2017
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 $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;
93
94 case $user_register_conf == 'visitors':
95 _user_mail_notify('register_no_approval_required', $account);
96 break;
97
98 case 'visitors_admin_approval':
99 _user_mail_notify('register_pending_approval', $account);
100 break;
101 }
102
103 return $account->id();
104 }
105
106 /**
107 * @inheritDoc
108 */
109 public function updateCMSName($ufID, $email) {
110 $user = entity_load('user', $ufID);
111 if ($user && $user->getEmail() != $email) {
112 $user->setEmail($email);
113
114 if (!count($user->validate())) {
115 $user->save();
116 }
117 }
118 }
119
120 /**
121 * Check if username and email exists in the drupal db.
122 *
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'.
129 */
130 public static function checkUserNameEmailExists(&$params, &$errors, $emailName = 'email') {
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.
141 $violations = array_filter($violations, function ($v) {
142 return $v->getPropertyPath() == 'name.0.value';
143 });
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.
159 $violations = array_filter($violations, function ($v) {
160 return $v->getPropertyPath() == 'mail.0.value';
161 });
162 if (count($violations) > 0) {
163 $errors[$emailName] = $violations[0]->getMessage();
164 }
165 }
166 }
167
168 /**
169 * @inheritDoc
170 */
171 public function getLoginURL($destination = '') {
172 $query = $destination ? array('destination' => $destination) : array();
173 return \Drupal::url('user.page', array(), array('query' => $query));
174 }
175
176 /**
177 * @inheritDoc
178 */
179 public function setTitle($title, $pageTitle = NULL) {
180 if (!$pageTitle) {
181 $pageTitle = $title;
182 }
183 \Drupal::service('civicrm.page_state')->setTitle($pageTitle);
184 }
185
186 /**
187 * @inheritDoc
188 */
189 public function appendBreadCrumb($breadcrumbs) {
190 $civicrmPageState = \Drupal::service('civicrm.page_state');
191 foreach ($breadcrumbs as $breadcrumb) {
192 $civicrmPageState->addBreadcrumb($breadcrumb['title'], $breadcrumb['url']);
193 }
194 }
195
196 /**
197 * @inheritDoc
198 */
199 public function resetBreadCrumb() {
200 \Drupal::service('civicrm.page_state')->resetBreadcrumbs();
201 }
202
203 /**
204 * @inheritDoc
205 */
206 public function addHTMLHead($header) {
207 \Drupal::service('civicrm.page_state')->addHtmlHeader($header);
208 }
209
210 /**
211 * @inheritDoc
212 */
213 public function addScriptUrl($url, $region) {
214 static $weight = 0;
215
216 switch ($region) {
217 case 'html-header':
218 case 'page-footer':
219 break;
220
221 default:
222 return FALSE;
223 }
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);
234 return TRUE;
235 }
236
237 /**
238 * @inheritDoc
239 */
240 public function addScript($code, $region) {
241 switch ($region) {
242 case 'html-header':
243 case 'page-footer':
244 break;
245
246 default:
247 return FALSE;
248 }
249
250 $script = array(
251 '#tag' => 'script',
252 '#value' => $code,
253 );
254 \Drupal::service('civicrm.page_state')->addJS($script);
255 return TRUE;
256 }
257
258 /**
259 * @inheritDoc
260 */
261 public function addStyleUrl($url, $region) {
262 if ($region != 'html-header') {
263 return FALSE;
264 }
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);
273 return TRUE;
274 }
275
276 /**
277 * @inheritDoc
278 */
279 public function addStyle($code, $region) {
280 if ($region != 'html-header') {
281 return FALSE;
282 }
283 $css = array(
284 '#tag' => 'style',
285 '#value' => $code,
286 );
287 \Drupal::service('civicrm.page_state')->addCSS($css);
288 return TRUE;
289 }
290
291 /**
292 * Check if a resource url is within the drupal directory and format appropriately.
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 *
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 *
300 * @param $url
301 *
302 * @return bool
303 */
304 public function formatResourceUrl(&$url) {
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 }
312 // FIXME: Should not unconditionally return true
313 return TRUE;
314 }
315
316 /**
317 * This function does nothing in Drupal 8. Changes to the base_url should be made
318 * in settings.php directly.
319 */
320 public function mapConfigToSSL() {
321 }
322
323 /**
324 * @inheritDoc
325 */
326 public function url(
327 $path = '',
328 $query = '',
329 $absolute = FALSE,
330 $fragment = NULL,
331 $frontend = FALSE,
332 $forceBackend = FALSE
333 ) {
334 $query = html_entity_decode($query);
335
336 $url = \Drupal\civicrm\CivicrmHelper::parseURL("{$path}?{$query}");
337
338 // Not all links that CiviCRM generates are Drupal routes, so we use the weaker ::fromUri method.
339 try {
340 $url = \Drupal\Core\Url::fromUri("base:{$url['path']}", array(
341 'query' => $url['query'],
342 'fragment' => $fragment,
343 'absolute' => $absolute,
344 ))->toString();
345 }
346 catch (Exception $e) {
347 // @Todo: log to watchdog
348 $url = '';
349 }
350
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
359 return $url;
360 }
361
362 /**
363 * @inheritDoc
364 */
365 public function authenticate($name, $password, $loadCMSBootstrap = FALSE, $realPath = NULL) {
366 $system = new CRM_Utils_System_Drupal8();
367 $system->loadBootStrap(array(), FALSE);
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 /**
376 * @inheritDoc
377 */
378 public function loadUser($username) {
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 /**
398 * Determine the native ID of the CMS user.
399 *
400 * @param string $username
401 * @return int|NULL
402 */
403 public function getUfId($username) {
404 if ($id = user_load_by_name($username)->id()) {
405 return $id;
406 }
407 }
408
409 /**
410 * @inheritDoc
411 */
412 public function permissionDenied() {
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 */
420 public function logout() {
421 // Pass
422 }
423
424 /**
425 * Load drupal bootstrap.
426 *
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
434 *
435 * @return bool
436 * @Todo Handle setting cleanurls configuration for CiviCRM?
437 */
438 public function loadBootStrap($params = array(), $loadUser = TRUE, $throwError = TRUE, $realPath = NULL) {
439 static $run_once = FALSE;
440 if ($run_once) {
441 return TRUE;
442 }
443 else {
444 $run_once = TRUE;
445 }
446
447 if (!($root = $this->cmsRootPath())) {
448 return FALSE;
449 }
450 chdir($root);
451
452 // Create a mock $request object
453 $autoloader = require_once $root . '/vendor/autoload.php';
454 if ($autoloader === TRUE) {
455 $autoloader = ComposerAutoloaderInitDrupal8::getLoader();
456 }
457 // @Todo: do we need to handle case where $_SERVER has no HTTP_HOST key, ie. when run via cli?
458 $request = new \Symfony\Component\HttpFoundation\Request(array(), array(), array(), array(), array(), $_SERVER);
459
460 // Create a kernel and boot it.
461 \Drupal\Core\DrupalKernel::createFromRequest($request, $autoloader, 'prod')->prepareLegacyRequest($request);
462
463 // Initialize Civicrm
464 \Drupal::service('civicrm');
465
466 // We need to call the config hook again, since we now know
467 // all the modules that are listening on it (CRM-8655).
468 CRM_Utils_Hook::config($config);
469
470 if ($loadUser) {
471 if (!empty($params['uid']) && $username = \Drupal\user\Entity\User::load($uid)->getUsername()) {
472 $this->loadUser($username);
473 }
474 elseif (!empty($params['name']) && !empty($params['pass']) && $this->authenticate($params['name'], $params['pass'])) {
475 $this->loadUser($params['name']);
476 }
477 }
478 return TRUE;
479 }
480
481 /**
482 * Determine the location of the CMS root.
483 *
484 * @param string $path
485 *
486 * @return NULL|string
487 */
488 public function cmsRootPath($path = NULL) {
489 global $civicrm_paths;
490 if (!empty($civicrm_paths['cms.root']['path'])) {
491 return $civicrm_paths['cms.root']['path'];
492 }
493
494 if (defined('DRUPAL_ROOT')) {
495 return DRUPAL_ROOT;
496 }
497
498 // It looks like Drupal hasn't been bootstrapped.
499 // We're going to attempt to discover the root Drupal path
500 // by climbing out of the folder hierarchy and looking around to see
501 // if we've found the Drupal root directory.
502 if (!$path) {
503 $path = $_SERVER['SCRIPT_FILENAME'];
504 }
505
506 // Normalize and explode path into its component paths.
507 $paths = explode(DIRECTORY_SEPARATOR, realpath($path));
508
509 // Remove script filename from array of directories.
510 array_pop($paths);
511
512 while (count($paths)) {
513 $candidate = implode('/', $paths);
514 if (file_exists($candidate . "/core/includes/bootstrap.inc")) {
515 return $candidate;
516 }
517
518 array_pop($paths);
519 }
520 }
521
522 /**
523 * @inheritDoc
524 */
525 public function isUserLoggedIn() {
526 return \Drupal::currentUser()->isAuthenticated();
527 }
528
529 /**
530 * @inheritDoc
531 */
532 public function isUserRegistrationPermitted() {
533 if (\Drupal::config('user.settings')->get('register') == 'admin_only') {
534 return FALSE;
535 }
536 return TRUE;
537 }
538
539 /**
540 * @inheritDoc
541 */
542 public function isPasswordUserGenerated() {
543 if (\Drupal::config('user.settings')->get('verify_mail') == TRUE) {
544 return FALSE;
545 }
546 return TRUE;
547 }
548
549 /**
550 * @inheritDoc
551 */
552 public function getLoggedInUfID() {
553 if ($id = \Drupal::currentUser()->id()) {
554 return $id;
555 }
556 }
557
558 /**
559 * @inheritDoc
560 */
561 public function getDefaultBlockLocation() {
562 return 'sidebar_first';
563 }
564
565 /**
566 * @inheritDoc
567 */
568 public function flush() {
569 // CiviCRM and Drupal both provide (different versions of) Symfony (and possibly share other classes too).
570 // If we call drupal_flush_all_caches(), Drupal will attempt to rediscover all of its classes, use Civicrm's
571 // alternatives instead and then die. Instead, we only clear cache bins and no more.
572 foreach (Drupal\Core\Cache\Cache::getBins() as $service_id => $cache_backend) {
573 $cache_backend->deleteAll();
574 }
575 }
576
577 /**
578 * @inheritDoc
579 */
580 public function getModules() {
581 $modules = array();
582
583 $module_data = system_rebuild_module_data();
584 foreach ($module_data as $module_name => $extension) {
585 if (!isset($extension->info['hidden']) && $extension->origin != 'core') {
586 $extension->schema_version = drupal_get_installed_schema_version($module_name);
587 $modules[] = new CRM_Core_Module('drupal.' . $module_name, ($extension->status == 1 ? TRUE : FALSE));
588 }
589 }
590 return $modules;
591 }
592
593 /**
594 * @inheritDoc
595 */
596 public function getUniqueIdentifierFromUserObject($user) {
597 return $user->get('mail')->value;
598 }
599
600 /**
601 * @inheritDoc
602 */
603 public function getUserIDFromUserObject($user) {
604 return $user->get('uid')->value;
605 }
606
607 /**
608 * @inheritDoc
609 */
610 public function synchronizeUsers() {
611 $config = CRM_Core_Config::singleton();
612 if (PHP_SAPI != 'cli') {
613 set_time_limit(300);
614 }
615
616 $users = array();
617 $users = \Drupal::entityTypeManager()->getStorage('user')->loadByProperties();
618
619 $uf = $config->userFramework;
620 $contactCount = 0;
621 $contactCreated = 0;
622 $contactMatching = 0;
623 foreach ($users as $user) {
624 $mail = $user->get('mail')->value;
625 if (empty($mail)) {
626 continue;
627 }
628 $uid = $user->get('uid')->value;
629 $contactCount++;
630 if ($match = CRM_Core_BAO_UFMatch::synchronizeUFMatch($user, $uid, $mail, $uf, 1, 'Individual', TRUE)) {
631 $contactCreated++;
632 }
633 else {
634 $contactMatching++;
635 }
636 if (is_object($match)) {
637 $match->free();
638 }
639 }
640
641 return array(
642 'contactCount' => $contactCount,
643 'contactMatching' => $contactMatching,
644 'contactCreated' => $contactCreated,
645 );
646 }
647
648 /**
649 * Drupal 8 has a different function to get current path, hence
650 * overriding the postURL function
651 *
652 * @param string $action
653 *
654 * @return string
655 */
656 public function postURL($action) {
657 if (!empty($action)) {
658 return $action;
659 }
660 $current_path = \Drupal::service('path.current')->getPath();
661 return $this->url($current_path);
662 }
663
664 }