1<?php
2
3namespace Illuminate\Session;
4
5use Illuminate\Contracts\Auth\Guard;
6use Illuminate\Contracts\Container\Container;
7use Illuminate\Database\ConnectionInterface;
8use Illuminate\Database\QueryException;
9use Illuminate\Support\Arr;
10use Illuminate\Support\Carbon;
11use Illuminate\Support\InteractsWithTime;
12use SessionHandlerInterface;
13
14class DatabaseSessionHandler implements ExistenceAwareInterface, SessionHandlerInterface
15{
16    use InteractsWithTime;
17
18    /**
19     * The database connection instance.
20     *
21     * @var \Illuminate\Database\ConnectionInterface
22     */
23    protected $connection;
24
25    /**
26     * The name of the session table.
27     *
28     * @var string
29     */
30    protected $table;
31
32    /**
33     * The number of minutes the session should be valid.
34     *
35     * @var int
36     */
37    protected $minutes;
38
39    /**
40     * The container instance.
41     *
42     * @var \Illuminate\Contracts\Container\Container
43     */
44    protected $container;
45
46    /**
47     * The existence state of the session.
48     *
49     * @var bool
50     */
51    protected $exists;
52
53    /**
54     * Create a new database session handler instance.
55     *
56     * @param  \Illuminate\Database\ConnectionInterface  $connection
57     * @param  string  $table
58     * @param  int  $minutes
59     * @param  \Illuminate\Contracts\Container\Container|null  $container
60     * @return void
61     */
62    public function __construct(ConnectionInterface $connection, $table, $minutes, Container $container = null)
63    {
64        $this->table = $table;
65        $this->minutes = $minutes;
66        $this->container = $container;
67        $this->connection = $connection;
68    }
69
70    /**
71     * {@inheritdoc}
72     */
73    public function open($savePath, $sessionName)
74    {
75        return true;
76    }
77
78    /**
79     * {@inheritdoc}
80     */
81    public function close()
82    {
83        return true;
84    }
85
86    /**
87     * {@inheritdoc}
88     */
89    public function read($sessionId)
90    {
91        $session = (object) $this->getQuery()->find($sessionId);
92
93        if ($this->expired($session)) {
94            $this->exists = true;
95
96            return '';
97        }
98
99        if (isset($session->payload)) {
100            $this->exists = true;
101
102            return base64_decode($session->payload);
103        }
104
105        return '';
106    }
107
108    /**
109     * Determine if the session is expired.
110     *
111     * @param  \stdClass  $session
112     * @return bool
113     */
114    protected function expired($session)
115    {
116        return isset($session->last_activity) &&
117            $session->last_activity < Carbon::now()->subMinutes($this->minutes)->getTimestamp();
118    }
119
120    /**
121     * {@inheritdoc}
122     */
123    public function write($sessionId, $data)
124    {
125        $payload = $this->getDefaultPayload($data);
126
127        if (! $this->exists) {
128            $this->read($sessionId);
129        }
130
131        if ($this->exists) {
132            $this->performUpdate($sessionId, $payload);
133        } else {
134            $this->performInsert($sessionId, $payload);
135        }
136
137        return $this->exists = true;
138    }
139
140    /**
141     * Perform an insert operation on the session ID.
142     *
143     * @param  string  $sessionId
144     * @param  string  $payload
145     * @return bool|null
146     */
147    protected function performInsert($sessionId, $payload)
148    {
149        try {
150            return $this->getQuery()->insert(Arr::set($payload, 'id', $sessionId));
151        } catch (QueryException $e) {
152            $this->performUpdate($sessionId, $payload);
153        }
154    }
155
156    /**
157     * Perform an update operation on the session ID.
158     *
159     * @param  string  $sessionId
160     * @param  string  $payload
161     * @return int
162     */
163    protected function performUpdate($sessionId, $payload)
164    {
165        return $this->getQuery()->where('id', $sessionId)->update($payload);
166    }
167
168    /**
169     * Get the default payload for the session.
170     *
171     * @param  string  $data
172     * @return array
173     */
174    protected function getDefaultPayload($data)
175    {
176        $payload = [
177            'payload' => base64_encode($data),
178            'last_activity' => $this->currentTime(),
179        ];
180
181        if (! $this->container) {
182            return $payload;
183        }
184
185        return tap($payload, function (&$payload) {
186            $this->addUserInformation($payload)
187                 ->addRequestInformation($payload);
188        });
189    }
190
191    /**
192     * Add the user information to the session payload.
193     *
194     * @param  array  $payload
195     * @return $this
196     */
197    protected function addUserInformation(&$payload)
198    {
199        if ($this->container->bound(Guard::class)) {
200            $payload['user_id'] = $this->userId();
201        }
202
203        return $this;
204    }
205
206    /**
207     * Get the currently authenticated user's ID.
208     *
209     * @return mixed
210     */
211    protected function userId()
212    {
213        return $this->container->make(Guard::class)->id();
214    }
215
216    /**
217     * Add the request information to the session payload.
218     *
219     * @param  array  $payload
220     * @return $this
221     */
222    protected function addRequestInformation(&$payload)
223    {
224        if ($this->container->bound('request')) {
225            $payload = array_merge($payload, [
226                'ip_address' => $this->ipAddress(),
227                'user_agent' => $this->userAgent(),
228            ]);
229        }
230
231        return $this;
232    }
233
234    /**
235     * Get the IP address for the current request.
236     *
237     * @return string
238     */
239    protected function ipAddress()
240    {
241        return $this->container->make('request')->ip();
242    }
243
244    /**
245     * Get the user agent for the current request.
246     *
247     * @return string
248     */
249    protected function userAgent()
250    {
251        return substr((string) $this->container->make('request')->header('User-Agent'), 0, 500);
252    }
253
254    /**
255     * {@inheritdoc}
256     */
257    public function destroy($sessionId)
258    {
259        $this->getQuery()->where('id', $sessionId)->delete();
260
261        return true;
262    }
263
264    /**
265     * {@inheritdoc}
266     */
267    public function gc($lifetime)
268    {
269        $this->getQuery()->where('last_activity', '<=', $this->currentTime() - $lifetime)->delete();
270    }
271
272    /**
273     * Get a fresh query builder instance for the table.
274     *
275     * @return \Illuminate\Database\Query\Builder
276     */
277    protected function getQuery()
278    {
279        return $this->connection->table($this->table);
280    }
281
282    /**
283     * Set the existence state for the session.
284     *
285     * @param  bool  $value
286     * @return $this
287     */
288    public function setExists($value)
289    {
290        $this->exists = $value;
291
292        return $this;
293    }
294}
295