Chris@0
|
1 <?php
|
Chris@0
|
2
|
Chris@0
|
3 namespace Drupal\Core\Flood;
|
Chris@0
|
4
|
Chris@0
|
5 use Drupal\Core\Database\SchemaObjectExistsException;
|
Chris@0
|
6 use Symfony\Component\HttpFoundation\RequestStack;
|
Chris@0
|
7 use Drupal\Core\Database\Connection;
|
Chris@0
|
8
|
Chris@0
|
9 /**
|
Chris@0
|
10 * Defines the database flood backend. This is the default Drupal backend.
|
Chris@0
|
11 */
|
Chris@0
|
12 class DatabaseBackend implements FloodInterface {
|
Chris@0
|
13
|
Chris@0
|
14 /**
|
Chris@0
|
15 * The database table name.
|
Chris@0
|
16 */
|
Chris@0
|
17 const TABLE_NAME = 'flood';
|
Chris@0
|
18
|
Chris@0
|
19 /**
|
Chris@0
|
20 * The database connection used to store flood event information.
|
Chris@0
|
21 *
|
Chris@0
|
22 * @var \Drupal\Core\Database\Connection
|
Chris@0
|
23 */
|
Chris@0
|
24 protected $connection;
|
Chris@0
|
25
|
Chris@0
|
26 /**
|
Chris@0
|
27 * The request stack.
|
Chris@0
|
28 *
|
Chris@0
|
29 * @var \Symfony\Component\HttpFoundation\RequestStack
|
Chris@0
|
30 */
|
Chris@0
|
31 protected $requestStack;
|
Chris@0
|
32
|
Chris@0
|
33 /**
|
Chris@0
|
34 * Construct the DatabaseBackend.
|
Chris@0
|
35 *
|
Chris@0
|
36 * @param \Drupal\Core\Database\Connection $connection
|
Chris@0
|
37 * The database connection which will be used to store the flood event
|
Chris@0
|
38 * information.
|
Chris@0
|
39 * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
|
Chris@0
|
40 * The request stack used to retrieve the current request.
|
Chris@0
|
41 */
|
Chris@0
|
42 public function __construct(Connection $connection, RequestStack $request_stack) {
|
Chris@0
|
43 $this->connection = $connection;
|
Chris@0
|
44 $this->requestStack = $request_stack;
|
Chris@0
|
45 }
|
Chris@0
|
46
|
Chris@0
|
47 /**
|
Chris@0
|
48 * {@inheritdoc}
|
Chris@0
|
49 */
|
Chris@0
|
50 public function register($name, $window = 3600, $identifier = NULL) {
|
Chris@0
|
51 if (!isset($identifier)) {
|
Chris@0
|
52 $identifier = $this->requestStack->getCurrentRequest()->getClientIp();
|
Chris@0
|
53 }
|
Chris@0
|
54 $try_again = FALSE;
|
Chris@0
|
55 try {
|
Chris@0
|
56 $this->doInsert($name, $window, $identifier);
|
Chris@0
|
57 }
|
Chris@0
|
58 catch (\Exception $e) {
|
Chris@0
|
59 $try_again = $this->ensureTableExists();
|
Chris@0
|
60 if (!$try_again) {
|
Chris@0
|
61 throw $e;
|
Chris@0
|
62 }
|
Chris@0
|
63 }
|
Chris@0
|
64 if ($try_again) {
|
Chris@0
|
65 $this->doInsert($name, $window, $identifier);
|
Chris@0
|
66 }
|
Chris@0
|
67 }
|
Chris@0
|
68
|
Chris@0
|
69 /**
|
Chris@0
|
70 * Inserts an event into the flood table
|
Chris@0
|
71 *
|
Chris@0
|
72 * @param string $name
|
Chris@0
|
73 * The name of an event.
|
Chris@0
|
74 * @param int $window
|
Chris@0
|
75 * Number of seconds before this event expires.
|
Chris@0
|
76 * @param string $identifier
|
Chris@0
|
77 * Unique identifier of the current user.
|
Chris@0
|
78 *
|
Chris@0
|
79 * @see \Drupal\Core\Flood\DatabaseBackend::register
|
Chris@0
|
80 */
|
Chris@0
|
81 protected function doInsert($name, $window, $identifier) {
|
Chris@0
|
82 $this->connection->insert(static::TABLE_NAME)
|
Chris@0
|
83 ->fields([
|
Chris@0
|
84 'event' => $name,
|
Chris@0
|
85 'identifier' => $identifier,
|
Chris@0
|
86 'timestamp' => REQUEST_TIME,
|
Chris@0
|
87 'expiration' => REQUEST_TIME + $window,
|
Chris@0
|
88 ])
|
Chris@0
|
89 ->execute();
|
Chris@0
|
90 }
|
Chris@0
|
91
|
Chris@0
|
92 /**
|
Chris@0
|
93 * {@inheritdoc}
|
Chris@0
|
94 */
|
Chris@0
|
95 public function clear($name, $identifier = NULL) {
|
Chris@0
|
96 if (!isset($identifier)) {
|
Chris@0
|
97 $identifier = $this->requestStack->getCurrentRequest()->getClientIp();
|
Chris@0
|
98 }
|
Chris@0
|
99 try {
|
Chris@0
|
100 $this->connection->delete(static::TABLE_NAME)
|
Chris@0
|
101 ->condition('event', $name)
|
Chris@0
|
102 ->condition('identifier', $identifier)
|
Chris@0
|
103 ->execute();
|
Chris@0
|
104 }
|
Chris@0
|
105 catch (\Exception $e) {
|
Chris@0
|
106 $this->catchException($e);
|
Chris@0
|
107 }
|
Chris@0
|
108 }
|
Chris@0
|
109
|
Chris@0
|
110 /**
|
Chris@0
|
111 * {@inheritdoc}
|
Chris@0
|
112 */
|
Chris@0
|
113 public function isAllowed($name, $threshold, $window = 3600, $identifier = NULL) {
|
Chris@0
|
114 if (!isset($identifier)) {
|
Chris@0
|
115 $identifier = $this->requestStack->getCurrentRequest()->getClientIp();
|
Chris@0
|
116 }
|
Chris@0
|
117 try {
|
Chris@0
|
118 $number = $this->connection->select(static::TABLE_NAME, 'f')
|
Chris@0
|
119 ->condition('event', $name)
|
Chris@0
|
120 ->condition('identifier', $identifier)
|
Chris@0
|
121 ->condition('timestamp', REQUEST_TIME - $window, '>')
|
Chris@0
|
122 ->countQuery()
|
Chris@0
|
123 ->execute()
|
Chris@0
|
124 ->fetchField();
|
Chris@0
|
125 return ($number < $threshold);
|
Chris@0
|
126 }
|
Chris@0
|
127 catch (\Exception $e) {
|
Chris@0
|
128 $this->catchException($e);
|
Chris@0
|
129 return TRUE;
|
Chris@0
|
130 }
|
Chris@0
|
131 }
|
Chris@0
|
132
|
Chris@0
|
133 /**
|
Chris@0
|
134 * {@inheritdoc}
|
Chris@0
|
135 */
|
Chris@0
|
136 public function garbageCollection() {
|
Chris@0
|
137 try {
|
Chris@0
|
138 $return = $this->connection->delete(static::TABLE_NAME)
|
Chris@0
|
139 ->condition('expiration', REQUEST_TIME, '<')
|
Chris@0
|
140 ->execute();
|
Chris@0
|
141 }
|
Chris@0
|
142 catch (\Exception $e) {
|
Chris@0
|
143 $this->catchException($e);
|
Chris@0
|
144 }
|
Chris@0
|
145 }
|
Chris@0
|
146
|
Chris@0
|
147 /**
|
Chris@0
|
148 * Check if the flood table exists and create it if not.
|
Chris@0
|
149 */
|
Chris@0
|
150 protected function ensureTableExists() {
|
Chris@0
|
151 try {
|
Chris@0
|
152 $database_schema = $this->connection->schema();
|
Chris@0
|
153 if (!$database_schema->tableExists(static::TABLE_NAME)) {
|
Chris@0
|
154 $schema_definition = $this->schemaDefinition();
|
Chris@0
|
155 $database_schema->createTable(static::TABLE_NAME, $schema_definition);
|
Chris@0
|
156 return TRUE;
|
Chris@0
|
157 }
|
Chris@0
|
158 }
|
Chris@0
|
159 // If another process has already created the table, attempting to create
|
Chris@0
|
160 // it will throw an exception. In this case just catch the exception and do
|
Chris@0
|
161 // nothing.
|
Chris@0
|
162 catch (SchemaObjectExistsException $e) {
|
Chris@0
|
163 return TRUE;
|
Chris@0
|
164 }
|
Chris@0
|
165 return FALSE;
|
Chris@0
|
166 }
|
Chris@0
|
167
|
Chris@0
|
168 /**
|
Chris@0
|
169 * Act on an exception when flood might be stale.
|
Chris@0
|
170 *
|
Chris@0
|
171 * If the table does not yet exist, that's fine, but if the table exists and
|
Chris@0
|
172 * yet the query failed, then the flood is stale and the exception needs to
|
Chris@0
|
173 * propagate.
|
Chris@0
|
174 *
|
Chris@0
|
175 * @param $e
|
Chris@0
|
176 * The exception.
|
Chris@0
|
177 *
|
Chris@0
|
178 * @throws \Exception
|
Chris@0
|
179 */
|
Chris@0
|
180 protected function catchException(\Exception $e) {
|
Chris@0
|
181 if ($this->connection->schema()->tableExists(static::TABLE_NAME)) {
|
Chris@0
|
182 throw $e;
|
Chris@0
|
183 }
|
Chris@0
|
184 }
|
Chris@0
|
185
|
Chris@0
|
186 /**
|
Chris@0
|
187 * Defines the schema for the flood table.
|
Chris@0
|
188 *
|
Chris@0
|
189 * @internal
|
Chris@0
|
190 */
|
Chris@0
|
191 public function schemaDefinition() {
|
Chris@0
|
192 return [
|
Chris@0
|
193 'description' => 'Flood controls the threshold of events, such as the number of contact attempts.',
|
Chris@0
|
194 'fields' => [
|
Chris@0
|
195 'fid' => [
|
Chris@0
|
196 'description' => 'Unique flood event ID.',
|
Chris@0
|
197 'type' => 'serial',
|
Chris@0
|
198 'not null' => TRUE,
|
Chris@0
|
199 ],
|
Chris@0
|
200 'event' => [
|
Chris@0
|
201 'description' => 'Name of event (e.g. contact).',
|
Chris@0
|
202 'type' => 'varchar_ascii',
|
Chris@0
|
203 'length' => 64,
|
Chris@0
|
204 'not null' => TRUE,
|
Chris@0
|
205 'default' => '',
|
Chris@0
|
206 ],
|
Chris@0
|
207 'identifier' => [
|
Chris@0
|
208 'description' => 'Identifier of the visitor, such as an IP address or hostname.',
|
Chris@0
|
209 'type' => 'varchar_ascii',
|
Chris@0
|
210 'length' => 128,
|
Chris@0
|
211 'not null' => TRUE,
|
Chris@0
|
212 'default' => '',
|
Chris@0
|
213 ],
|
Chris@0
|
214 'timestamp' => [
|
Chris@0
|
215 'description' => 'Timestamp of the event.',
|
Chris@0
|
216 'type' => 'int',
|
Chris@0
|
217 'not null' => TRUE,
|
Chris@0
|
218 'default' => 0,
|
Chris@0
|
219 ],
|
Chris@0
|
220 'expiration' => [
|
Chris@0
|
221 'description' => 'Expiration timestamp. Expired events are purged on cron run.',
|
Chris@0
|
222 'type' => 'int',
|
Chris@0
|
223 'not null' => TRUE,
|
Chris@0
|
224 'default' => 0,
|
Chris@0
|
225 ],
|
Chris@0
|
226 ],
|
Chris@0
|
227 'primary key' => ['fid'],
|
Chris@0
|
228 'indexes' => [
|
Chris@0
|
229 'allow' => ['event', 'identifier', 'timestamp'],
|
Chris@0
|
230 'purge' => ['expiration'],
|
Chris@0
|
231 ],
|
Chris@0
|
232 ];
|
Chris@0
|
233 }
|
Chris@0
|
234
|
Chris@0
|
235 }
|