SqlRedirectNotFoundStorage.php 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. <?php
  2. namespace Drupal\redirect_404;
  3. use Drupal\Component\Utility\Unicode;
  4. use Drupal\Core\Config\ConfigFactoryInterface;
  5. use Drupal\Core\Database\Connection;
  6. /**
  7. * Provides an SQL implementation for redirect not found storage.
  8. *
  9. * To keep a limited amount of relevant records, we compute a relevancy based
  10. * on the amount of visits for each row, deleting the less visited record and
  11. * sorted by timestamp.
  12. */
  13. class SqlRedirectNotFoundStorage implements RedirectNotFoundStorageInterface {
  14. /**
  15. * Maximum column length for invalid paths.
  16. */
  17. const MAX_PATH_LENGTH = 191;
  18. /**
  19. * Active database connection.
  20. *
  21. * @var \Drupal\Core\Database\Connection
  22. */
  23. protected $database;
  24. /**
  25. * The configuration factory.
  26. *
  27. * @var \Drupal\Core\Config\ConfigFactoryInterface
  28. */
  29. protected $configFactory;
  30. /**
  31. * Constructs a new SqlRedirectNotFoundStorage.
  32. *
  33. * @param \Drupal\Core\Database\Connection $database
  34. * A Database connection to use for reading and writing database data.
  35. * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
  36. * The configuration factory.
  37. */
  38. public function __construct(Connection $database, ConfigFactoryInterface $config_factory) {
  39. $this->database = $database;
  40. $this->configFactory = $config_factory;
  41. }
  42. /**
  43. * {@inheritdoc}
  44. */
  45. public function logRequest($path, $langcode) {
  46. if (mb_strlen($path) > static::MAX_PATH_LENGTH) {
  47. // Don't attempt to log paths that would result in an exception. There is
  48. // no point in logging truncated paths, as they cannot be used to build a
  49. // new redirect.
  50. return;
  51. }
  52. // Ignore invalid UTF-8, which can't be logged.
  53. if (!Unicode::validateUtf8($path)) {
  54. return;
  55. }
  56. // If the request is not new, update its count and timestamp.
  57. $this->database->merge('redirect_404')
  58. ->key('path', $path)
  59. ->key('langcode', $langcode)
  60. ->expression('count', 'count + 1')
  61. ->fields([
  62. 'timestamp' => REQUEST_TIME,
  63. 'count' => 1,
  64. 'resolved' => 0,
  65. ])
  66. ->execute();
  67. }
  68. /**
  69. * {@inheritdoc}
  70. */
  71. public function resolveLogRequest($path, $langcode) {
  72. $this->database->update('redirect_404')
  73. ->fields(['resolved' => 1])
  74. ->condition('path', $path)
  75. ->condition('langcode', $langcode)
  76. ->execute();
  77. }
  78. /**
  79. * {@inheritdoc}
  80. */
  81. public function purgeOldRequests() {
  82. $row_limit = $this->configFactory->get('redirect_404.settings')->get('row_limit');
  83. // In admin form 0 used as value for 'All' label.
  84. if ($row_limit == 0) {
  85. return;
  86. }
  87. $query = $this->database->select('redirect_404', 'r404');
  88. $query->fields('r404', ['timestamp']);
  89. // On databases known to support log(), use it to calculate a logarithmic
  90. // scale of the count, to delete records with count of 1-9 first, then
  91. // 10-99 and so on.
  92. if ($this->database->driver() == 'mysql' || $this->database->driver() == 'pgsql') {
  93. $query->addExpression('floor(log(10, count))', 'count_log');
  94. $query->orderBy('count_log', 'DESC');
  95. }
  96. $query->orderBy('timestamp', 'DESC');
  97. $cutoff = $query
  98. ->range($row_limit, 1)
  99. ->execute()
  100. ->fetchAssoc();
  101. if (!empty($cutoff)) {
  102. // Delete records having older timestamp and less visits (on a logarithmic
  103. // scale) than cutoff.
  104. $delete_query = $this->database->delete('redirect_404');
  105. if ($this->database->driver() == 'mysql' || $this->database->driver() == 'pgsql') {
  106. // Delete rows with same count_log AND older timestamp than cutoff.
  107. $and_condition = $delete_query->andConditionGroup()
  108. ->where('floor(log(10, count)) = :count_log2', [':count_log2' => $cutoff['count_log']])
  109. ->condition('timestamp', $cutoff['timestamp'], '<=');
  110. // And delete all the rows with count_log less than the cutoff.
  111. $condition = $delete_query->orConditionGroup()
  112. ->where('floor(log(10, count)) < :count_log1', [':count_log1' => $cutoff['count_log']])
  113. ->condition($and_condition);
  114. $delete_query->condition($condition);
  115. }
  116. else {
  117. $delete_query->condition('timestamp', $cutoff['timestamp'], '<=');
  118. }
  119. $delete_query->execute();
  120. }
  121. }
  122. /**
  123. * {@inheritdoc}
  124. */
  125. public function listRequests(array $header = [], $search = NULL) {
  126. $query = $this->database
  127. ->select('redirect_404', 'r404')
  128. ->extend('Drupal\Core\Database\Query\TableSortExtender')
  129. ->orderByHeader($header)
  130. ->extend('Drupal\Core\Database\Query\PagerSelectExtender')
  131. ->limit(25)
  132. ->fields('r404');
  133. if ($search) {
  134. // Replace wildcards with PDO wildcards.
  135. // @todo Find a way to write a nicer pattern.
  136. $wildcard = '%' . trim(preg_replace('!\*+!', '%', $this->database->escapeLike($search)), '%') . '%';
  137. $query->condition('path', $wildcard, 'LIKE');
  138. }
  139. $results = $query->condition('resolved', 0, '=')->execute()->fetchAll();
  140. return $results;
  141. }
  142. }