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