DatabaseBackend.php 6.1 KB

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