Completed
Branch 2.0 (13ec26)
by Anton
05:17
created

SessionMiddleware::process()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 17
rs 9.7
c 0
b 0
f 0
cc 1
nc 1
nop 2
1
<?php
2
/**
3
 * Spiral Framework.
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
9
namespace Spiral\Session\Middleware;
10
11
use Psr\Http\Message\ResponseInterface as Response;
12
use Psr\Http\Message\ServerRequestInterface as Request;
13
use Psr\Http\Message\UriInterface;
14
use Psr\Http\Server\MiddlewareInterface;
15
use Psr\Http\Server\RequestHandlerInterface as Handler;
16
use Spiral\Core\ScopeInterface;
17
use Spiral\Http\Configs\HttpConfig;
18
use Spiral\Http\Cookies\Cookie;
19
use Spiral\Session\Configs\SessionConfig;
20
use Spiral\Session\SessionFactory;
21
use Spiral\Session\SessionInterface;
22
23
class SessionMiddleware implements MiddlewareInterface
24
{
25
    const ATTRIBUTE = 'session';
26
27
    // Header set used to sign session
28
    const SIGNATURE_HEADERS = ['User-Agent', 'Accept-Language', 'Accept-Encoding'];
29
30
    /** @var SessionConfig */
31
    private $config;
32
33
    /** @var HttpConfig */
34
    private $httpConfig;
35
36
    /** @var SessionFactory */
37
    private $factory;
38
39
    /** @var ScopeInterface */
40
    private $scope;
41
42
    /**
43
     * @param SessionConfig  $config
44
     * @param HttpConfig     $httpConfig
45
     * @param SessionFactory $factory
46
     * @param ScopeInterface $scope
47
     */
48
    public function __construct(
49
        SessionConfig $config,
50
        HttpConfig $httpConfig,
51
        SessionFactory $factory,
52
        ScopeInterface $scope
53
    ) {
54
        $this->config = $config;
55
        $this->httpConfig = $httpConfig;
56
        $this->factory = $factory;
57
        $this->scope = $scope;
58
    }
59
60
    /**
61
     * @inheritdoc
62
     */
63
    public function process(Request $request, Handler $handler): Response
64
    {
65
        //Initiating session, this can only be done once!
66
        $session = $this->factory->initSession(
67
            $this->clientSignature($request),
68
            $this->fetchID($request)
69
        );
70
71
        $response = $this->scope->runScope(
72
            [SessionInterface::class => $session],
73
            function () use ($session, $request, $handler) {
74
                return $handler->handle($request->withAttribute(static::ATTRIBUTE, $session));
75
            }
76
        );
77
78
        return $this->commitSession($session, $request, $response);
79
    }
80
81
    /**
82
     * @param SessionInterface $session
83
     * @param Request          $request
84
     * @param Response         $response
85
     *
86
     * @return Response
87
     */
88
    protected function commitSession(
89
        SessionInterface $session,
90
        Request $request,
91
        Response $response
92
    ): Response {
93
        if (!$session->isStarted()) {
94
            return $response;
95
        }
96
97
        $session->commit();
98
99
        //SID changed
100
        if ($this->fetchID($request) != $session->getID()) {
101
            return $this->withCookie($request, $response, $session->getID());
102
        }
103
104
        //Nothing to do
105
        return $response;
106
    }
107
108
    /**
109
     * Attempt to locate session ID in request.
110
     *
111
     * @param Request $request
112
     *
113
     * @return string|null
114
     */
115
    protected function fetchID(Request $request): ?string
116
    {
117
        $cookies = $request->getCookieParams();
118
        if (empty($cookies[$this->config->getCookie()])) {
119
            return null;
120
        }
121
122
        return $cookies[$this->config->getCookie()];
123
    }
124
125
    /**
126
     * @param Request  $request
127
     * @param Response $response
128
     * @param string   $id
0 ignored issues
show
Documentation introduced by
Should the type for parameter $id not be null|string?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
129
     *
130
     * @return Response
131
     */
132
    protected function withCookie(Request $request, Response $response, string $id = null): Response
133
    {
134
        $response = $response->withAddedHeader(
135
            'Set-Cookie',
136
            $this->sessionCookie($request->getUri(), $id)->createHeader()
137
        );
138
139
        return $response;
140
    }
141
142
    /**
143
     * Must return string which identifies client on other end. Not for security check but for
144
     * session fixation.
145
     *
146
     * @param Request $request
147
     *
148
     * @return string
149
     */
150
    protected function clientSignature(Request $request): string
151
    {
152
        $signature = '';
153
        foreach (static::SIGNATURE_HEADERS as $header) {
154
            $signature .= $request->getHeaderLine($header) . ';';
155
        }
156
157
        return hash('sha256', $signature);
158
    }
159
160
    /**
161
     * Generate session cookie.
162
     *
163
     * @param UriInterface $uri Incoming uri.
164
     * @param string|null  $id
165
     *
166
     * @return Cookie
167
     */
168
    private function sessionCookie(UriInterface $uri, string $id = null): Cookie
169
    {
170
        return Cookie::create(
171
            $this->config->getCookie(),
172
            $id,
173
            $this->config->getLifetime(),
174
            $this->httpConfig->basePath(),
175
            $this->httpConfig->cookieDomain($uri),
176
            $this->config->isSecure(),
177
            true
178
        );
179
    }
180
}