Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit b919fed

Browse filesBrowse files
committed
BC Concurrent Sessions Control
1 parent 38179de commit b919fed
Copy full SHA for b919fed
Expand file treeCollapse file tree

35 files changed

+1539
-412
lines changed

‎src/Symfony/Bridge/Doctrine/Security/SessionRegistry/Schema.php

Copy file name to clipboardExpand all lines: src/Symfony/Bridge/Doctrine/Security/SessionRegistry/Schema.php
+6-5Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,27 +17,28 @@
1717
* The schema used for the ACL system.
1818
*
1919
* @author Stefan Paschke <stefan.paschke@gmail.com>
20+
* @author Antonio J. García Lagar <aj@garcialagar.es>
2021
*/
2122
final class Schema extends BaseSchema
2223
{
2324
/**
2425
* Constructor
2526
*
26-
* @param array $options the names for tables
27+
* @param string $table
2728
*/
28-
public function __construct(array $options)
29+
public function __construct($table)
2930
{
3031
parent::__construct();
3132

32-
$this->addSessionInformationTable($options);
33+
$this->addSessionInformationTable($table);
3334
}
3435

3536
/**
3637
* Adds the session_information table to the schema
3738
*/
38-
protected function addSessionInformationTable(array $options)
39+
protected function addSessionInformationTable($table)
3940
{
40-
$table = $this->createTable($options['session_information_table_name']);
41+
$table = $this->createTable($table);
4142
$table->addColumn('session_id', 'string');
4243
$table->addColumn('username', 'string');
4344
$table->addColumn('expired', 'datetime', array('unsigned' => true, 'notnull' => false));

‎src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionInformation.php

Copy file name to clipboardExpand all lines: src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionInformation.php
-42Lines changed: 0 additions & 42 deletions
This file was deleted.

‎src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php

Copy file name to clipboardExpand all lines: src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php
+95-36Lines changed: 95 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,39 +2,35 @@
22

33
namespace Symfony\Bridge\Doctrine\Security\SessionRegistry;
44

5-
use Doctrine\DBAL\Driver\Connection;
5+
use Doctrine\DBAL\Connection;
66
use Symfony\Component\Security\Http\Session\SessionInformation;
77
use Symfony\Component\Security\Http\Session\SessionRegistryStorageInterface;
88

9+
/**
10+
* @author Stefan Paschke <stefan.paschke@gmail.com>
11+
* @author Antonio J. García Lagar <aj@garcialagar.es>
12+
*/
913
class SessionRegistryStorage implements SessionRegistryStorageInterface
1014
{
1115
protected $connection;
12-
protected $options;
16+
protected $table;
1317

14-
public function __construct(Connection $connection, array $options)
18+
public function __construct(Connection $connection, $table)
1519
{
16-
$this->connection = $connection;
17-
$this->options = $options;
18-
}
19-
20-
/**
21-
* not implemented
22-
*/
23-
public function getUsers()
24-
{
25-
throw new \BadMethodCallException("Not implemented.");
20+
$this->connection = $connection;
21+
$this->table = $table;
2622
}
2723

2824
/**
2925
* Obtains the maintained information for one session.
3026
*
31-
* @param string $sessionId the session identifier key.
27+
* @param string $sessionId the session identifier key.
3228
* @return SessionInformation a SessionInformation object.
3329
*/
3430
public function getSessionInformation($sessionId)
3531
{
3632
$statement = $this->connection->executeQuery(
37-
'SELECT * FROM '.$this->options['session_information_table_name'].' WHERE session_id = :session_id',
33+
'SELECT * FROM '.$this->table.' WHERE session_id = :session_id',
3834
array('session_id' => $sessionId)
3935
);
4036

@@ -46,24 +42,23 @@ public function getSessionInformation($sessionId)
4642
/**
4743
* Obtains the maintained information for one user.
4844
*
49-
* @param string $username The user identifier.
50-
* @param boolean $includeExpiredSessions.
51-
* @return array An array of SessionInformation objects.
45+
* @param string $username The user identifier.
46+
* @param boolean $includeExpiredSessions.
47+
* @return array An array of SessionInformation objects.
5248
*/
5349
public function getSessionInformations($username, $includeExpiredSessions = false)
5450
{
5551
$sessionInformations = array();
5652

5753
$statement = $this->connection->executeQuery(
5854
'SELECT *
59-
FROM '.$this->options['session_information_table_name'].'
55+
FROM '.$this->table.'
6056
WHERE username = :username'.($includeExpiredSessions ? '' : ' AND expired IS NULL ').'
6157
ORDER BY last_request DESC',
6258
array('username' => $username)
6359
);
6460

65-
while ($data = $statement->fetch(\PDO::FETCH_ASSOC))
66-
{
61+
while ($data = $statement->fetch(\PDO::FETCH_ASSOC)) {
6762
$sessionInformations[] = $this->instantiateSessionInformationFromResultSet($data);
6863
}
6964

@@ -78,17 +73,52 @@ public function getSessionInformations($username, $includeExpiredSessions = fals
7873
*/
7974
public function setSessionInformation(SessionInformation $sessionInformation)
8075
{
81-
$statement = $this->connection->prepare(
82-
'INSERT INTO '.$this->options['session_information_table_name'].'
83-
(session_id, username, last_request, expired) VALUES(:session_id, :username, :last_request, :expired)
84-
ON DUPLICATE KEY
85-
UPDATE username=:username, last_request=:last_request, expired=:expired');
86-
87-
$statement->bindValue('session_id', $sessionInformation->getSessionId());
88-
$statement->bindValue('username', $sessionInformation->getUsername());
89-
$statement->bindValue('last_request', $sessionInformation->getLastRequest(), 'datetime');
90-
$statement->bindValue('expired', $sessionInformation->getExpired(), 'datetime');
91-
$statement->execute();
76+
$mergeSql = $this->getMergeSql();
77+
78+
if (null !== $mergeSql) {
79+
$mergeStmt = $this->pdo->prepare($mergeSql);
80+
$mergeStmt->bindValue('session_id', $sessionInformation->getSessionId());
81+
$mergeStmt->bindValue('username', $sessionInformation->getUsername());
82+
$mergeStmt->bindValue('last_request', $sessionInformation->getLastRequest(), 'datetime');
83+
$mergeStmt->bindValue('expired', $sessionInformation->getExpired(), 'datetime');
84+
$mergeStmt->execute();
85+
86+
return true;
87+
}
88+
89+
$updateStmt = $this->pdo->prepare(
90+
"UPDATE $this->table SET username=:username, last_request=:last_request, expired=:expired WHERE session_id = :session_id"
91+
);
92+
$mergeStmt->bindValue('session_id', $sessionInformation->getSessionId());
93+
$mergeStmt->bindValue('username', $sessionInformation->getUsername());
94+
$mergeStmt->bindValue('last_request', $sessionInformation->getLastRequest(), 'datetime');
95+
$mergeStmt->bindValue('expired', $sessionInformation->getExpired(), 'datetime');
96+
$updateStmt->execute();
97+
98+
// When MERGE is not supported, like in Postgres, we have to use this approach that can result in
99+
// duplicate key errors when the same sessioninfo is written simultaneously. We can just catch such an
100+
// error and re-execute the update. This is similar to a serializable transaction with retry logic
101+
// on serialization failures but without the overhead and without possible false positives due to
102+
// longer gap locking.
103+
if (!$updateStmt->rowCount()) {
104+
try {
105+
$insertStmt = $this->pdo->prepare(
106+
"INTO $this->table (session_id, username, last_request, expired) VALUES (:session_id, :username, :last_request, :expired)"
107+
);
108+
$insertStmt->bindValue('session_id', $sessionInformation->getSessionId());
109+
$insertStmt->bindValue('username', $sessionInformation->getUsername());
110+
$insertStmt->bindValue('last_request', $sessionInformation->getLastRequest(), 'datetime');
111+
$insertStmt->bindValue('expired', $sessionInformation->getExpired(), 'datetime');
112+
$insertStmt->execute();
113+
} catch (\PDOException $e) {
114+
// Handle integrity violation SQLSTATE 23000 (or a subclass like 23505 in Postgres) for duplicate keys
115+
if (0 === strpos($e->getCode(), '23')) {
116+
$updateStmt->execute();
117+
} else {
118+
throw $e;
119+
}
120+
}
121+
}
92122
}
93123

94124
/**
@@ -98,16 +128,45 @@ public function setSessionInformation(SessionInformation $sessionInformation)
98128
*/
99129
public function removeSessionInformation($sessionId)
100130
{
101-
$this->connection->delete($this->options['session_information_table_name'], array('session_id' => $sessionId));
131+
$this->connection->delete($this->table, array('session_id' => $sessionId));
102132
}
103133

104134
private function instantiateSessionInformationFromResultSet($data)
105135
{
106-
return new $this->options['session_information_class_name'](
136+
return new SessionInformation(
107137
$data['session_id'],
108138
$data['username'],
109-
(null == $data['last_request']) ? null : new \DateTime($data['last_request']),
110-
(null == $data['expired']) ? null : new \DateTime($data['expired'])
139+
null === $data['last_request'] ? null : new \DateTime($data['last_request']),
140+
null === $data['expired'] ? null : new \DateTime($data['expired'])
111141
);
112142
}
143+
144+
/**
145+
* Returns a merge/upsert (i.e. insert or update) SQL query when supported by the database.
146+
*
147+
* @return string|null The SQL string or null when not supported
148+
*/
149+
private function getMergeSql()
150+
{
151+
switch ($this->connection->getDriver()->getName()) {
152+
case 'pdo_mysql':
153+
return "INSERT INTO $this->table (session_id, username, last_request, expired) VALUES (:session_id, :username, :last_request, :expired) " .
154+
"ON DUPLICATE KEY UPDATE username = VALUES(username), last_request = VALUES(last_request), expired = VALUES(expired)";
155+
case 'pdo_oracle':
156+
// DUAL is Oracle specific dummy table
157+
return "MERGE INTO $this->table USING DUAL ON (session_id= :session_id) " .
158+
"WHEN NOT MATCHED THEN INSERT (session_id, username, last_request, expired) VALUES (:session_id, :username, :last_request, :expired) " .
159+
"WHEN MATCHED THEN UPDATE SET username = :username, last_request = :last_request, expired = :expired";
160+
case 'pdo_sqlsrv':
161+
if (version_compare($this->connection->getWrappedConnection()->getAttribute(\PDO::ATTR_SERVER_VERSION), '10', '>=')) {
162+
// MERGE is only available since SQL Server 2008 and must be terminated by semicolon
163+
// It also requires HOLDLOCK according to http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx
164+
return "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON (session_id = :session_id) " .
165+
"WHEN NOT MATCHED THEN INSERT (session_id, username, last_request, expired) VALUES (:session_id, :username, :last_request, :expired) " .
166+
"WHEN MATCHED THEN UPDATE SET username = :username, last_request = :last_request, expired = :expired;";
167+
}
168+
case 'pdo_sqlite':
169+
return "INSERT OR REPLACE INTO $this->table (session_id, username, last_request, expired) VALUES (:session_id, :username, :last_request, :expired)";
170+
}
171+
}
113172
}

‎src/Symfony/Bundle/SecurityBundle/Command/InitConcurrentSessionsCommand.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/Command/InitConcurrentSessionsCommand.php
+7-16Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
* Installs the database schema required by the concurrent session Doctrine implementation
2121
*
2222
* @author Stefan Paschke <stefan.paschke@gmail.com>
23+
* @author Antonio J. García Lagar <aj@garcialagar.es>
2324
*/
2425
class InitConcurrentSessionsCommand extends ContainerAwareCommand
2526
{
@@ -28,8 +29,6 @@ class InitConcurrentSessionsCommand extends ContainerAwareCommand
2829
*/
2930
protected function configure()
3031
{
31-
parent::configure();
32-
3332
$this
3433
->setName('init:concurrent-session')
3534
->setDescription('Executes the SQL needed to generate the database schema required by the concurrent sessions feature.')
@@ -38,10 +37,6 @@ protected function configure()
3837
generate the database schema required by the concurrent session Doctrine implementation:
3938
4039
<info>./app/console init:concurrent-session</info>
41-
42-
You can also output the SQL instead of executing it:
43-
44-
<info>./app/console init:concurrent-session --dump-sql</info>
4540
EOT
4641
);
4742
}
@@ -54,23 +49,19 @@ protected function execute(InputInterface $input, OutputInterface $output)
5449
$connection = $this->getContainer()->get('security.session_registry.dbal.connection');
5550
$sm = $connection->getSchemaManager();
5651
$tableNames = $sm->listTableNames();
57-
$tables = array(
58-
'session_information_table_name' => $this->getContainer()->getParameter('security.session_registry.dbal.session_information_table_name'),
59-
);
52+
$table = $this->getContainer()->getParameter('security.session_registry.dbal.session_information_table_name');
6053

61-
foreach ($tables as $table) {
62-
if (in_array($table, $tableNames, true)) {
63-
$output->writeln(sprintf('The table "%s" already exists. Aborting.', $table));
54+
if (in_array($table, $tableNames, true)) {
55+
$output->writeln(sprintf('The table "%s" already exists. Aborting.', $table));
6456

65-
return;
66-
}
57+
return;
6758
}
6859

69-
$schema = new Schema($tables);
60+
$schema = new Schema($table);
7061
foreach ($schema->toSql($connection->getDatabasePlatform()) as $sql) {
7162
$connection->exec($sql);
7263
}
7364

74-
$output->writeln('concurrent session tables have been initialized successfully.');
65+
$output->writeln('concurrent session table have been initialized successfully.');
7566
}
7667
}

‎src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php
+2-6Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,7 @@ private function addSessionRegistrySection(ArrayNodeDefinition $rootNode)
9191
->arrayNode('session_registry')
9292
->children()
9393
->scalarNode('connection')->end()
94-
->arrayNode('tables')
95-
->addDefaultsIfNotSet()
96-
->children()
97-
->scalarNode('session_information')->defaultValue('cs_session_information')->end()
98-
->end()
99-
->end()
94+
->scalarNode('table')->defaultValue('cs_session_information')->end()
10095
->scalarNode('session_registry_storage')->end()
10196
->end()
10297
->end()
@@ -313,6 +308,7 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto
313308
->canBeUnset()
314309
->children()
315310
->scalarNode('max_sessions')->defaultNull()->end()
311+
->booleanNode('error_if_maximum_exceeded')->defaultTrue()->end()
316312
->scalarNode('expiration_url')->defaultValue('/')->end()
317313
->end()
318314
->end()

‎src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php
+10-4Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,13 @@ abstract class AbstractFactory implements SecurityFactoryInterface
4747
'failure_path_parameter' => '_failure_path',
4848
);
4949

50-
public function create(ContainerBuilder $container, $id, $config, $userProviderId, $defaultEntryPointId, $sessionStrategy)
50+
public function create(ContainerBuilder $container, $id, $config, $userProviderId, $defaultEntryPointId)
5151
{
5252
// authentication provider
5353
$authProviderId = $this->createAuthProvider($container, $id, $config, $userProviderId);
5454

5555
// authentication listener
56-
$listenerId = $this->createListener($container, $id, $config, $userProviderId, $sessionStrategy);
56+
$listenerId = $this->createListener($container, $id, $config, $userProviderId);
5757

5858
// add remember-me aware tag if requested
5959
if ($this->isRememberMeAware($config)) {
@@ -153,11 +153,17 @@ protected function isRememberMeAware($config)
153153
return $config['remember_me'];
154154
}
155155

156-
protected function createListener($container, $id, $config, $userProvider, $sessionStrategy)
156+
protected function createListener($container, $id, $config, $userProvider)
157157
{
158158
$listenerId = $this->getListenerId();
159159
$listener = new DefinitionDecorator($listenerId);
160-
$listener->replaceArgument(2, $sessionStrategy);
160+
161+
//Check for custom session authentication strategy
162+
$sessionAuthenticationStrategyId = 'security.authentication.session_strategy.'.$id;
163+
if ($container->hasDefinition($sessionAuthenticationStrategyId) || $container->hasAlias($sessionAuthenticationStrategyId)) {
164+
$listener->replaceArgument(2, new Reference($sessionAuthenticationStrategyId));
165+
}
166+
161167
$listener->replaceArgument(4, $id);
162168
$listener->replaceArgument(5, new Reference($this->createAuthenticationSuccessHandler($container, $id, $config)));
163169
$listener->replaceArgument(6, new Reference($this->createAuthenticationFailureHandler($container, $id, $config)));

‎src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php
+2-2Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,9 @@ protected function createAuthProvider(ContainerBuilder $container, $id, $config,
7171
return $provider;
7272
}
7373

74-
protected function createListener($container, $id, $config, $userProvider, $sessionStrategy)
74+
protected function createListener($container, $id, $config, $userProvider)
7575
{
76-
$listenerId = parent::createListener($container, $id, $config, $userProvider, $sessionStrategy);
76+
$listenerId = parent::createListener($container, $id, $config, $userProvider);
7777

7878
$container
7979
->getDefinition($listenerId)

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.