ip_login-4.x-dev/src/StackMiddleware/EarlyIpLoginMiddleware.php
src/StackMiddleware/EarlyIpLoginMiddleware.php
<?php
namespace Drupal\ip_login\StackMiddleware;
use Drupal\ip_login\IpLoginController;
use Psr\Container\ContainerInterface;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface;
/**
* Provides a HTTP middleware to implement IP based login.
*
* The role of this "early" middleware is to determine if a user can be logged
* in automatically. If so, a request attribute is set and IpLoginMiddleware
* does the actual login, because that needs to happen after the Drupal kernel
* is initialized by \Drupal\Core\StackMiddleware\KernelPreHandle.
*/
class EarlyIpLoginMiddleware implements HttpKernelInterface {
/**
* The service container.
*/
protected ContainerInterface $container;
/**
* The decorated kernel.
*
* @var \Symfony\Component\HttpKernel\HttpKernelInterface
*/
protected $httpKernel;
/**
* The session service name.
*
* @var string
*/
protected $sessionServiceName;
/**
* Cache the front page path.
*
* @var string
*/
protected $frontPage;
/**
* Constructs an EarlyIpLoginMiddleware.
*
* @param \Symfony\Component\HttpKernel\HttpKernelInterface $http_kernel
* The decorated kernel.
* @param string $service_name
* The name of the session service, defaults to "session".
*/
public function __construct(HttpKernelInterface $http_kernel, $service_name = 'session') {
$this->httpKernel = $http_kernel;
$this->sessionServiceName = $service_name;
}
/**
* Sets the service container.
*/
public function setContainer(ContainerInterface $container): void {
$this->container = $container;
}
/**
* {@inheritdoc}
*/
public function handle(Request $request, $type = self::MAIN_REQUEST, $catch = TRUE): Response {
// Bail out early if we already determined that we can not auto-login.
if ($request->cookies->get('ipLoginAttempted', NULL)) {
return $this->httpKernel->handle($request, $type, $catch);
}
$uid = NULL;
if ($type === self::MAIN_REQUEST && PHP_SAPI !== 'cli') {
// Put the current (unprepared) request on the stack so we can initialize
// the session.
$this->container->get('request_stack')->push($request);
$session = $this->container->get($this->sessionServiceName);
$session->start();
$uid = $session->get('uid');
// Remove the unprepared request from the stack,
// \Drupal\Core\StackMiddleware\KernelPreHandle::handle() adds the proper
// one.
$this->container->get('request_stack')->pop();
}
// Do nothing if the user is logged in, or if this is not a web request.
if ($uid || PHP_SAPI === 'cli') {
return $this->httpKernel->handle($request, $type, $catch);
}
if ($this->ipLoginCheckPath($request) === FALSE) {
return $this->httpKernel->handle($request, $type, $catch);
}
// Check the user's IP.
if ($matched_uid = IpLoginController::checkIpLoginExists($request)) {
// For clarity about every scenario, use extensive logic.
$can_login_as_another_user = $request->cookies->get('ipLoginAsDifferentUser', NULL);
if ($can_login_as_another_user === NULL) {
// First time login for user, so log in automatically.
$request->attributes->set('ip_login_uid', $matched_uid);
}
elseif ($can_login_as_another_user == FALSE) {
// User logged out, but is not allowed to use another user, so log in
// again.
$request->attributes->set('ip_login_uid', $matched_uid);
}
elseif ($can_login_as_another_user == TRUE) {
// User logged out, and is allowed to login as another user, so do
// nothing, just stay on this page and wait for user action.
}
else {
// Do automatic login.
$request->attributes->set('ip_login_uid', $matched_uid);
}
}
$response = $this->httpKernel->handle($request, $type, $catch);
// If we determined that we can't auto-login the user, set a session cookie
// so we don't repeat the user IP check for this browser session.
if (empty($matched_uid)) {
$response->headers->setCookie(new Cookie('ipLoginAttempted', 1));
}
return $response;
}
/**
* Checks path of current page matches to see if IP login should occur.
*
* @return bool
* True if the path matches the configured pages, false otherwise.
*/
private function ipLoginCheckPath(Request $request): bool {
$config = $this->container->get('config.factory')->get('ip_login.settings');
$check_mode = $config->get('check_mode');
$paths = $config->get('paths');
if (!empty($paths)) {
// Compare with the path with allowed pages.
$path = $request->getPathInfo();
if ($path === '/') {
// If the path is the front page, use the front page path from config.
$path = $this->getFrontPagePath();
}
$page_match = $this->matchPath($path, $paths);
// When $check_mode has a value of 0, the IP check happens on
// all paths except those listed in $paths. When set to 1, IPs
// are checked only on those paths listed in $paths.
$page_match = !($check_mode xor $page_match);
// If we don't have a path match, don't log in.
if (!$page_match) {
return FALSE;
}
}
// All is well, continue with login.
return TRUE;
}
/**
* Checks if a path matches any pattern in a set of patterns.
*
* @param string $path
* The path to match.
* @param string $patterns
* A set of patterns separated by a newline.
*
* @return bool
* TRUE if the path matches a pattern, FALSE otherwise.
*/
private function matchPath(string $path, string $patterns): bool {
// Convert path settings to a regular expression.
$to_replace = [
// Replace newlines with a logical 'or'.
'/(\r\n?|\n)/',
// Quote asterisks.
'/\\\\\*/',
// Quote <front> keyword.
'/(^|\|)\\\\<front\\\\>($|\|)/',
];
$replacements = [
'|',
'.*',
'\1' . preg_quote($this->getFrontPagePath(), '/') . '\2',
];
$patterns_quoted = preg_quote($patterns, '/');
$regex = '/^(' . preg_replace($to_replace, $replacements, $patterns_quoted) . ')$/';
return (bool) preg_match($regex, $path);
}
/**
* Gets the current front page path.
*
* @return string
* The front page path.
*/
protected function getFrontPagePath(): string {
// Lazy-load front page config.
if (!isset($this->frontPage)) {
$this->frontPage = $this->container->get('config.factory')
->get('system.site')
->get('page.front');
}
return $this->frontPage;
}
}
