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