Upgraded drupal core with security updates
[yaffs-website] / web / core / lib / Drupal / Core / Lock / DatabaseLockBackend.php
1 <?php
2
3 namespace Drupal\Core\Lock;
4
5 use Drupal\Core\Database\Connection;
6 use Drupal\Core\Database\IntegrityConstraintViolationException;
7 use Drupal\Core\Database\SchemaObjectExistsException;
8
9 /**
10  * Defines the database lock backend. This is the default backend in Drupal.
11  *
12  * @ingroup lock
13  */
14 class DatabaseLockBackend extends LockBackendAbstract {
15
16   /**
17    * The database table name.
18    */
19   const TABLE_NAME = 'semaphore';
20
21   /**
22    * The database connection.
23    *
24    * @var \Drupal\Core\Database\Connection
25    */
26   protected $database;
27
28   /**
29    * Constructs a new DatabaseLockBackend.
30    *
31    * @param \Drupal\Core\Database\Connection $database
32    *   The database connection.
33    */
34   public function __construct(Connection $database) {
35     // __destruct() is causing problems with garbage collections, register a
36     // shutdown function instead.
37     drupal_register_shutdown_function([$this, 'releaseAll']);
38     $this->database = $database;
39   }
40
41   /**
42    * {@inheritdoc}
43    */
44   public function acquire($name, $timeout = 30.0) {
45     // Insure that the timeout is at least 1 ms.
46     $timeout = max($timeout, 0.001);
47     $expire = microtime(TRUE) + $timeout;
48     if (isset($this->locks[$name])) {
49       // Try to extend the expiration of a lock we already acquired.
50       $success = (bool) $this->database->update('semaphore')
51         ->fields(['expire' => $expire])
52         ->condition('name', $name)
53         ->condition('value', $this->getLockId())
54         ->execute();
55       if (!$success) {
56         // The lock was broken.
57         unset($this->locks[$name]);
58       }
59       return $success;
60     }
61     else {
62       // Optimistically try to acquire the lock, then retry once if it fails.
63       // The first time through the loop cannot be a retry.
64       $retry = FALSE;
65       // We always want to do this code at least once.
66       do {
67         try {
68           $this->database->insert('semaphore')
69             ->fields([
70               'name' => $name,
71               'value' => $this->getLockId(),
72               'expire' => $expire,
73             ])
74             ->execute();
75           // We track all acquired locks in the global variable.
76           $this->locks[$name] = TRUE;
77           // We never need to try again.
78           $retry = FALSE;
79         }
80         catch (IntegrityConstraintViolationException $e) {
81           // Suppress the error. If this is our first pass through the loop,
82           // then $retry is FALSE. In this case, the insert failed because some
83           // other request acquired the lock but did not release it. We decide
84           // whether to retry by checking lockMayBeAvailable(). This will clear
85           // the offending row from the database table in case it has expired.
86           $retry = $retry ? FALSE : $this->lockMayBeAvailable($name);
87         }
88         catch (\Exception $e) {
89           // Create the semaphore table if it does not exist and retry.
90           if ($this->ensureTableExists()) {
91             // Retry only once.
92             $retry = !$retry;
93           }
94           else {
95             throw $e;
96           }
97         }
98         // We only retry in case the first attempt failed, but we then broke
99         // an expired lock.
100       } while ($retry);
101     }
102     return isset($this->locks[$name]);
103   }
104
105   /**
106    * {@inheritdoc}
107    */
108   public function lockMayBeAvailable($name) {
109     try {
110       $lock = $this->database->query('SELECT expire, value FROM {semaphore} WHERE name = :name', [':name' => $name])->fetchAssoc();
111     }
112     catch (\Exception $e) {
113       $this->catchException($e);
114       // If the table does not exist yet then the lock may be available.
115       $lock = FALSE;
116     }
117     if (!$lock) {
118       return TRUE;
119     }
120     $expire = (float) $lock['expire'];
121     $now = microtime(TRUE);
122     if ($now > $expire) {
123       // We check two conditions to prevent a race condition where another
124       // request acquired the lock and set a new expire time. We add a small
125       // number to $expire to avoid errors with float to string conversion.
126       return (bool) $this->database->delete('semaphore')
127         ->condition('name', $name)
128         ->condition('value', $lock['value'])
129         ->condition('expire', 0.0001 + $expire, '<=')
130         ->execute();
131     }
132     return FALSE;
133   }
134
135   /**
136    * {@inheritdoc}
137    */
138   public function release($name) {
139     unset($this->locks[$name]);
140     try {
141       $this->database->delete('semaphore')
142         ->condition('name', $name)
143         ->condition('value', $this->getLockId())
144         ->execute();
145     }
146     catch (\Exception $e) {
147       $this->catchException($e);
148     }
149   }
150
151   /**
152    * {@inheritdoc}
153    */
154   public function releaseAll($lock_id = NULL) {
155     // Only attempt to release locks if any were acquired.
156     if (!empty($this->locks)) {
157       $this->locks = [];
158       if (empty($lock_id)) {
159         $lock_id = $this->getLockId();
160       }
161       $this->database->delete('semaphore')
162         ->condition('value', $lock_id)
163         ->execute();
164     }
165   }
166
167   /**
168    * Check if the semaphore table exists and create it if not.
169    */
170   protected function ensureTableExists() {
171     try {
172       $database_schema = $this->database->schema();
173       if (!$database_schema->tableExists(static::TABLE_NAME)) {
174         $schema_definition = $this->schemaDefinition();
175         $database_schema->createTable(static::TABLE_NAME, $schema_definition);
176         return TRUE;
177       }
178     }
179     // If another process has already created the semaphore table, attempting to
180     // recreate it will throw an exception. In this case just catch the
181     // exception and do nothing.
182     catch (SchemaObjectExistsException $e) {
183       return TRUE;
184     }
185     return FALSE;
186   }
187
188   /**
189    * Act on an exception when semaphore might be stale.
190    *
191    * If the table does not yet exist, that's fine, but if the table exists and
192    * yet the query failed, then the semaphore is stale and the exception needs
193    * to propagate.
194    *
195    * @param $e
196    *   The exception.
197    *
198    * @throws \Exception
199    */
200   protected function catchException(\Exception $e) {
201     if ($this->database->schema()->tableExists(static::TABLE_NAME)) {
202       throw $e;
203     }
204   }
205
206   /**
207    * Defines the schema for the semaphore table.
208    */
209   public function schemaDefinition() {
210     return [
211       'description' => 'Table for holding semaphores, locks, flags, etc. that cannot be stored as state since they must not be cached.',
212       'fields' => [
213         'name' => [
214           'description' => 'Primary Key: Unique name.',
215           'type' => 'varchar_ascii',
216           'length' => 255,
217           'not null' => TRUE,
218           'default' => ''
219         ],
220         'value' => [
221           'description' => 'A value for the semaphore.',
222           'type' => 'varchar_ascii',
223           'length' => 255,
224           'not null' => TRUE,
225           'default' => ''
226         ],
227         'expire' => [
228           'description' => 'A Unix timestamp with microseconds indicating when the semaphore should expire.',
229           'type' => 'float',
230           'size' => 'big',
231           'not null' => TRUE
232         ],
233       ],
234       'indexes' => [
235         'value' => ['value'],
236         'expire' => ['expire'],
237       ],
238       'primary key' => ['name'],
239     ];
240   }
241
242 }