[TASK] Re-work/simplify copyright header in PHP files - Part 2
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Locking / Locker.php
1 <?php
2 namespace TYPO3\CMS\Core\Locking;
3
4 /**
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16 use TYPO3\CMS\Core\Utility\GeneralUtility;
17
18 /**
19 * TYPO3 locking class
20 * This class provides an abstract layer to various locking features for TYPO3
21 *
22 * It is intended to blocks requests until some data has been generated.
23 * This is especially useful if two clients are requesting the same website short after each other. While the request of client 1 triggers building and caching of the website, client 2 will be waiting at this lock.
24 *
25 * @author Michael Stucki <michael@typo3.org>
26 * @author Markus Klein <klein.t3@mfc-linz.at>
27 */
28 class Locker {
29
30 const LOCKING_METHOD_SIMPLE = 'simple';
31 const LOCKING_METHOD_FLOCK = 'flock';
32 const LOCKING_METHOD_SEMAPHORE = 'semaphore';
33 const LOCKING_METHOD_DISABLED = 'disable';
34
35 const FILE_LOCK_FOLDER = 'typo3temp/locks/';
36
37 /**
38 * @var string Locking method: One of the constants above
39 */
40 protected $method = '';
41
42 /**
43 * @var mixed Identifier used for this lock
44 */
45 protected $id;
46
47 /**
48 * @var mixed Resource used for this lock (can be a file or a semaphore resource)
49 */
50 protected $resource;
51
52 /**
53 * @var resource File pointer if using flock method
54 */
55 protected $filePointer;
56
57 /**
58 * @var boolean True if lock is acquired
59 */
60 protected $isAcquired = FALSE;
61
62 /**
63 * @var integer Number of times a locked resource is tried to be acquired. Only used in manual locks method "simple".
64 */
65 protected $loops = 150;
66
67 /**
68 * @var integer Milliseconds after lock acquire is retried. $loops * $step results in the maximum delay of a lock. Only used in manual lock method "simple".
69 */
70 protected $step = 200;
71
72 /**
73 * @var string Logging facility
74 */
75 protected $syslogFacility = 'cms';
76
77 /**
78 * @var boolean True if locking should be logged
79 */
80 protected $isLoggingEnabled = TRUE;
81
82 /**
83 * Constructor:
84 * initializes locking, check input parameters and set variables accordingly.
85 *
86 * Parameters $loops and $step only apply to the locking method LOCKING_METHOD_SIMPLE.
87 *
88 * @param string $id ID to identify this lock in the system
89 * @param string $method Define which locking method to use. Use one of the LOCKING_METHOD_* constants. Defaults to LOCKING_METHOD_SIMPLE. Use '' to use setting from Install Tool.
90 * @param int $loops Number of times a locked resource is tried to be acquired.
91 * @param int $step Milliseconds after lock acquire is retried. $loops * $step results in the maximum delay of a lock.
92 * @throws \RuntimeException
93 * @throws \InvalidArgumentException
94 */
95 public function __construct($id, $method = self::LOCKING_METHOD_SIMPLE, $loops = 0, $step = 0) {
96 // Force ID to be string
97 $id = (string)$id;
98 if ((int)$loops) {
99 $this->loops = (int)$loops;
100 }
101 if ((int)$step) {
102 $this->step = (int)$step;
103 }
104 if ($method === '' && isset($GLOBALS['TYPO3_CONF_VARS']['SYS']['lockingMode'])) {
105 $method = (string)$GLOBALS['TYPO3_CONF_VARS']['SYS']['lockingMode'];
106 }
107
108 switch ($method) {
109 case self::LOCKING_METHOD_SIMPLE:
110 // intended fall through
111 case self::LOCKING_METHOD_FLOCK:
112 $this->id = md5($id);
113 $this->createPathIfNeeded();
114 break;
115 case self::LOCKING_METHOD_SEMAPHORE:
116 $this->id = abs(crc32($id));
117 break;
118 case self::LOCKING_METHOD_DISABLED:
119 break;
120 default:
121 throw new \InvalidArgumentException('No such locking method "' . $method . '"', 1294586097);
122 }
123 $this->method = $method;
124 }
125
126 /**
127 * Destructor:
128 * Releases lock automatically when instance is destroyed and release resources
129 */
130 public function __destruct() {
131 $this->release();
132 switch ($this->method) {
133 case self::LOCKING_METHOD_FLOCK:
134 if (
135 GeneralUtility::isAllowedAbsPath($this->resource)
136 && GeneralUtility::isFirstPartOfStr($this->resource, PATH_site . self::FILE_LOCK_FOLDER)
137 ) {
138 @unlink($this->resource);
139 }
140 break;
141 case self::LOCKING_METHOD_SEMAPHORE:
142 @sem_remove($this->resource);
143 break;
144 default:
145 // do nothing
146 }
147 }
148
149 /**
150 * Tries to allocate the semaphore
151 *
152 * @return void
153 * @throws \RuntimeException
154 */
155 protected function getSemaphore() {
156 $this->resource = sem_get($this->id, 1);
157 if ($this->resource === FALSE) {
158 throw new \RuntimeException('Unable to get semaphore with id ' . $this->id, 1313828196);
159 }
160 }
161
162 /**
163 * Acquire a lock and return when successful.
164 *
165 * It is important to know that the lock will be acquired in any case, even if the request was blocked first.
166 * Therefore, the lock needs to be released in every situation.
167 *
168 * @return boolean Returns TRUE if lock could be acquired without waiting, FALSE otherwise.
169 * @throws \RuntimeException
170 * @deprecated since 6.2 - will be removed two versions later; use new API instead
171 */
172 public function acquire() {
173 // TODO refactor locking in TSFE to use the new API, then this call can be logged
174 // GeneralUtility::logDeprecatedFunction();
175
176 // Default is TRUE, which means continue without caring for other clients.
177 // In the case of TYPO3s cache management, this has no negative effect except some resource overhead.
178 $noWait = FALSE;
179 $isAcquired = FALSE;
180 switch ($this->method) {
181 case self::LOCKING_METHOD_SIMPLE:
182 if (file_exists($this->resource)) {
183 $this->sysLog('Waiting for a different process to release the lock');
184 $maxExecutionTime = (int)ini_get('max_execution_time');
185 $maxAge = time() - ($maxExecutionTime ?: 120);
186 if (@filectime($this->resource) < $maxAge) {
187 @unlink($this->resource);
188 $this->sysLog('Unlinking stale lockfile');
189 }
190 }
191 for ($i = 0; $i < $this->loops; $i++) {
192 $filePointer = @fopen($this->resource, 'x');
193 if ($filePointer !== FALSE) {
194 fclose($filePointer);
195 GeneralUtility::fixPermissions($this->resource);
196 $this->sysLog('Lock acquired');
197 $noWait = $i === 0;
198 $isAcquired = TRUE;
199 break;
200 }
201 usleep($this->step * 1000);
202 }
203 if (!$isAcquired) {
204 throw new \RuntimeException('Lock file could not be created', 1294586098);
205 }
206 break;
207 case self::LOCKING_METHOD_FLOCK:
208 $this->filePointer = fopen($this->resource, 'c');
209 if ($this->filePointer === FALSE) {
210 throw new \RuntimeException('Lock file could not be opened', 1294586099);
211 }
212 // Lock without blocking
213 if (flock($this->filePointer, LOCK_EX | LOCK_NB)) {
214 $noWait = TRUE;
215 } elseif (flock($this->filePointer, LOCK_EX)) {
216 // Lock with blocking (waiting for similar locks to become released)
217 $noWait = FALSE;
218 } else {
219 throw new \RuntimeException('Could not lock file "' . $this->resource . '"', 1294586100);
220 }
221 $isAcquired = TRUE;
222 break;
223 case self::LOCKING_METHOD_SEMAPHORE:
224 $this->getSemaphore();
225 while (!$isAcquired) {
226 if (@sem_acquire($this->resource)) {
227 // Unfortunately it is not possible to find out if the request has blocked,
228 // as sem_acquire will block until we get the resource.
229 // So we do not set $noWait here at all
230 $isAcquired = TRUE;
231 }
232 }
233 break;
234 case self::LOCKING_METHOD_DISABLED:
235 break;
236 default:
237 // will never be reached
238 }
239 $this->isAcquired = $isAcquired;
240 return $noWait;
241 }
242
243 /**
244 * Try to acquire an exclusive lock
245 *
246 * @throws \RuntimeException
247 * @return bool Returns TRUE if the lock was acquired successfully
248 */
249 public function acquireExclusiveLock() {
250 if ($this->isAcquired) {
251 return TRUE;
252 }
253 $this->isAcquired = FALSE;
254 switch ($this->method) {
255 case self::LOCKING_METHOD_SIMPLE:
256 if (file_exists($this->resource)) {
257 $this->sysLog('Waiting for a different process to release the lock');
258 $maxExecutionTime = (int)ini_get('max_execution_time');
259 $maxAge = time() - ($maxExecutionTime ?: 120);
260 if (@filectime($this->resource) < $maxAge) {
261 @unlink($this->resource);
262 $this->sysLog('Unlinking stale lockfile');
263 }
264 }
265 for ($i = 0; $i < $this->loops; $i++) {
266 $filePointer = @fopen($this->resource, 'x');
267 if ($filePointer !== FALSE) {
268 fclose($filePointer);
269 GeneralUtility::fixPermissions($this->resource);
270 $this->sysLog('Lock acquired');
271 $this->isAcquired = TRUE;
272 break;
273 }
274 usleep($this->step * 1000);
275 }
276 break;
277 case self::LOCKING_METHOD_FLOCK:
278 $this->filePointer = fopen($this->resource, 'c');
279 if ($this->filePointer === FALSE) {
280 throw new \RuntimeException('Lock file could not be opened', 1294586099);
281 }
282 if (flock($this->filePointer, LOCK_EX)) {
283 $this->isAcquired = TRUE;
284 }
285 break;
286 case self::LOCKING_METHOD_SEMAPHORE:
287 $this->getSemaphore();
288 if (@sem_acquire($this->resource)) {
289 $this->isAcquired = TRUE;
290 }
291 break;
292 case self::LOCKING_METHOD_DISABLED:
293 break;
294 default:
295 // will never be reached
296 }
297 return $this->isAcquired;
298 }
299
300 /**
301 * Try to acquire a shared lock
302 *
303 * (Only works for the flock() locking method currently)
304 *
305 * @return bool Returns TRUE if the lock was acquired successfully
306 * @throws \RuntimeException
307 */
308 public function acquireSharedLock() {
309 if ($this->isAcquired) {
310 return TRUE;
311 }
312 $isAcquired = FALSE;
313 if ($this->method === self::LOCKING_METHOD_FLOCK) {
314 $this->filePointer = fopen($this->resource, 'c');
315 if ($this->filePointer === FALSE) {
316 throw new \RuntimeException('Lock file could not be opened', 1294586099);
317 }
318 if (flock($this->filePointer, LOCK_SH)) {
319 $isAcquired = TRUE;
320 }
321 }
322 return $isAcquired;
323 }
324
325 /**
326 * Release the lock
327 *
328 * @return boolean Returns TRUE on success or FALSE on failure
329 */
330 public function release() {
331 if (!$this->isAcquired) {
332 return TRUE;
333 }
334 $success = TRUE;
335 switch ($this->method) {
336 case self::LOCKING_METHOD_SIMPLE:
337 if (
338 GeneralUtility::isAllowedAbsPath($this->resource)
339 && GeneralUtility::isFirstPartOfStr($this->resource, PATH_site . self::FILE_LOCK_FOLDER)
340 ) {
341 if (@unlink($this->resource) === FALSE) {
342 $success = FALSE;
343 }
344 }
345 break;
346 case self::LOCKING_METHOD_FLOCK:
347 if (is_resource($this->filePointer)) {
348 if (flock($this->filePointer, LOCK_UN) === FALSE) {
349 $success = FALSE;
350 }
351 fclose($this->filePointer);
352 }
353 break;
354 case self::LOCKING_METHOD_SEMAPHORE:
355 if (!@sem_release($this->resource)) {
356 $success = FALSE;
357 }
358 break;
359 case self::LOCKING_METHOD_DISABLED:
360 break;
361 default:
362 // will never be reached
363 }
364 $this->isAcquired = FALSE;
365 return $success;
366 }
367
368 /**
369 * Return the locking method which is currently used
370 *
371 * @return string Locking method
372 */
373 public function getMethod() {
374 return $this->method;
375 }
376
377 /**
378 * Return the ID which is currently used
379 *
380 * @return string Locking ID
381 */
382 public function getId() {
383 return $this->id;
384 }
385
386 /**
387 * Return the resource which is currently used.
388 * Depending on the locking method this can be a filename or a semaphore resource.
389 *
390 * @return mixed Locking resource (filename as string or semaphore as resource)
391 */
392 public function getResource() {
393 return $this->resource;
394 }
395
396 /**
397 * Return the local status of a lock
398 *
399 * @return bool Returns TRUE if lock is acquired by this process, FALSE otherwise
400 */
401 public function getLockStatus() {
402 return $this->isAcquired;
403 }
404
405 /**
406 * Return the global status of the lock
407 *
408 * @return bool Returns TRUE if the lock is locked by either this or another process, FALSE otherwise
409 */
410 public function isLocked() {
411 $result = FALSE;
412 switch ($this->method) {
413 case self::LOCKING_METHOD_SIMPLE:
414 if (file_exists($this->resource)) {
415 $maxExecutionTime = (int)ini_get('max_execution_time');
416 $maxAge = time() - ($maxExecutionTime ?: 120);
417 if (@filectime($this->resource) < $maxAge) {
418 @unlink($this->resource);
419 $this->sysLog('Unlinking stale lockfile');
420 } else {
421 $result = TRUE;
422 }
423 }
424 break;
425 case self::LOCKING_METHOD_FLOCK:
426 // we can't detect this reliably here, since the third parameter of flock() does not work on windows
427 break;
428 case self::LOCKING_METHOD_SEMAPHORE:
429 // no way to detect this at all, no PHP API for that
430 break;
431 case self::LOCKING_METHOD_DISABLED:
432 break;
433 default:
434 // will never be reached
435 }
436 return $result;
437 }
438
439 /**
440 * Sets the facility (extension name) for the syslog entry.
441 *
442 * @param string $syslogFacility
443 */
444 public function setSyslogFacility($syslogFacility) {
445 $this->syslogFacility = $syslogFacility;
446 }
447
448 /**
449 * Enable/ disable logging
450 *
451 * @param boolean $isLoggingEnabled
452 */
453 public function setEnableLogging($isLoggingEnabled) {
454 $this->isLoggingEnabled = $isLoggingEnabled;
455 }
456
457 /**
458 * Adds a common log entry for this locking API using \TYPO3\CMS\Core\Utility\GeneralUtility::sysLog().
459 * Example: 25-02-08 17:58 - cms: Locking [simple::0aeafd2a67a6bb8b9543fb9ea25ecbe2]: Acquired
460 *
461 * @param string $message The message to be logged
462 * @param integer $severity Severity - 0 is info (default), 1 is notice, 2 is warning, 3 is error, 4 is fatal error
463 * @return void
464 */
465 public function sysLog($message, $severity = 0) {
466 if ($this->isLoggingEnabled) {
467 GeneralUtility::sysLog('Locking [' . $this->method . '::' . $this->id . ']: ' . trim($message), $this->syslogFacility, $severity);
468 }
469 }
470
471 /**
472 * Tests if the directory for simple locks is available.
473 * If not, the directory will be created. The lock path is usually
474 * below typo3temp, typo3temp itself should exist already
475 *
476 * @return void
477 * @throws \RuntimeException If path couldn't be created.
478 */
479 protected function createPathIfNeeded() {
480 $path = PATH_site . self::FILE_LOCK_FOLDER;
481 if (!is_dir($path)) {
482 // Not using mkdir_deep on purpose here, if typo3temp itself
483 // does not exist, this issue should be solved on a different
484 // level of the application.
485 if (!GeneralUtility::mkdir($path)) {
486 throw new \RuntimeException('Cannot create directory ' . $path, 1395140007);
487 }
488 }
489 if (!is_writable($path)) {
490 throw new \RuntimeException('Cannot write to directory ' . $path, 1396278700);
491 }
492 $this->resource = $path . $this->id;
493 }
494 }