228 lines
6.3 KiB
PHP
228 lines
6.3 KiB
PHP
<?php
|
|
|
|
namespace Kirby\Cms;
|
|
|
|
use Kirby\Data\Data;
|
|
use Kirby\Exception\Exception;
|
|
use Kirby\Filesystem\F;
|
|
|
|
/**
|
|
* Manages all content lock files
|
|
*
|
|
* @package Kirby Cms
|
|
* @author Nico Hoffmann <nico@getkirby.com>,
|
|
* Lukas Bestle <lukas@getkirby.com>
|
|
* @link https://getkirby.com
|
|
* @copyright Bastian Allgeier
|
|
* @license https://getkirby.com/license
|
|
*/
|
|
class ContentLocks
|
|
{
|
|
/**
|
|
* Data from the `.lock` files
|
|
* that have been read so far
|
|
* cached by `.lock` file path
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $data = [];
|
|
|
|
/**
|
|
* PHP file handles for all currently
|
|
* open `.lock` files
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $handles = [];
|
|
|
|
/**
|
|
* Closes the open file handles
|
|
*
|
|
* @codeCoverageIgnore
|
|
*/
|
|
public function __destruct()
|
|
{
|
|
foreach ($this->handles as $file => $handle) {
|
|
$this->closeHandle($file);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes the file lock and closes the file handle
|
|
*
|
|
* @param string $file
|
|
* @return void
|
|
* @throws \Kirby\Exception\Exception
|
|
*/
|
|
protected function closeHandle(string $file)
|
|
{
|
|
if (isset($this->handles[$file]) === false) {
|
|
return;
|
|
}
|
|
|
|
$handle = $this->handles[$file];
|
|
$result = flock($handle, LOCK_UN) && fclose($handle);
|
|
|
|
if ($result !== true) {
|
|
throw new Exception('Unexpected file system error.'); // @codeCoverageIgnore
|
|
}
|
|
|
|
unset($this->handles[$file]);
|
|
}
|
|
|
|
/**
|
|
* Returns the path to a model's lock file
|
|
*
|
|
* @param \Kirby\Cms\ModelWithContent $model
|
|
* @return string
|
|
*/
|
|
public static function file(ModelWithContent $model): string
|
|
{
|
|
return $model->contentFileDirectory() . '/.lock';
|
|
}
|
|
|
|
/**
|
|
* Returns the lock/unlock data for the specified model
|
|
*
|
|
* @param \Kirby\Cms\ModelWithContent $model
|
|
* @return array
|
|
*/
|
|
public function get(ModelWithContent $model): array
|
|
{
|
|
$file = static::file($model);
|
|
$id = static::id($model);
|
|
|
|
// return from cache if file was already loaded
|
|
if (isset($this->data[$file]) === true) {
|
|
return $this->data[$file][$id] ?? [];
|
|
}
|
|
|
|
// first get a handle to ensure a file system lock
|
|
$handle = $this->handle($file);
|
|
|
|
if (is_resource($handle) === true) {
|
|
// read data from file
|
|
clearstatcache();
|
|
$filesize = filesize($file);
|
|
|
|
if ($filesize > 0) {
|
|
// always read the whole file
|
|
rewind($handle);
|
|
$string = fread($handle, $filesize);
|
|
$data = Data::decode($string, 'yaml');
|
|
}
|
|
}
|
|
|
|
$this->data[$file] = $data ?? [];
|
|
|
|
return $this->data[$file][$id] ?? [];
|
|
}
|
|
|
|
/**
|
|
* Returns the file handle to a `.lock` file
|
|
*
|
|
* @param string $file
|
|
* @param bool $create Whether to create the file if it does not exist
|
|
* @return resource|null File handle
|
|
* @throws \Kirby\Exception\Exception
|
|
*/
|
|
protected function handle(string $file, bool $create = false)
|
|
{
|
|
// check for an already open handle
|
|
if (isset($this->handles[$file]) === true) {
|
|
return $this->handles[$file];
|
|
}
|
|
|
|
// don't create a file if not requested
|
|
if (is_file($file) !== true && $create !== true) {
|
|
return null;
|
|
}
|
|
|
|
$handle = @fopen($file, 'c+b');
|
|
if (is_resource($handle) === false) {
|
|
throw new Exception('Lock file ' . $file . ' could not be opened.'); // @codeCoverageIgnore
|
|
}
|
|
|
|
// lock the lock file exclusively to prevent changes by other threads
|
|
$result = flock($handle, LOCK_EX);
|
|
if ($result !== true) {
|
|
throw new Exception('Unexpected file system error.'); // @codeCoverageIgnore
|
|
}
|
|
|
|
return $this->handles[$file] = $handle;
|
|
}
|
|
|
|
/**
|
|
* Returns model ID used as the key for the data array;
|
|
* prepended with a slash because the $site otherwise won't have an ID
|
|
*
|
|
* @param \Kirby\Cms\ModelWithContent $model
|
|
* @return string
|
|
*/
|
|
public static function id(ModelWithContent $model): string
|
|
{
|
|
return '/' . $model->id();
|
|
}
|
|
|
|
/**
|
|
* Sets and writes the lock/unlock data for the specified model
|
|
*
|
|
* @param \Kirby\Cms\ModelWithContent $model
|
|
* @param array $data
|
|
* @return bool
|
|
* @throws \Kirby\Exception\Exception
|
|
*/
|
|
public function set(ModelWithContent $model, array $data): bool
|
|
{
|
|
$file = static::file($model);
|
|
$id = static::id($model);
|
|
$handle = $this->handle($file, true);
|
|
|
|
$this->data[$file][$id] = $data;
|
|
|
|
// make sure to unset model id entries,
|
|
// if no lock data for the model exists
|
|
foreach ($this->data[$file] as $id => $data) {
|
|
// there is no data for that model whatsoever
|
|
if (
|
|
isset($data['lock']) === false &&
|
|
(isset($data['unlock']) === false ||
|
|
count($data['unlock']) === 0)
|
|
) {
|
|
unset($this->data[$file][$id]);
|
|
|
|
// there is empty unlock data, but still lock data
|
|
} elseif (
|
|
isset($data['unlock']) === true &&
|
|
count($data['unlock']) === 0
|
|
) {
|
|
unset($this->data[$file][$id]['unlock']);
|
|
}
|
|
}
|
|
|
|
// there is no data left in the file whatsoever, delete the file
|
|
if (count($this->data[$file]) === 0) {
|
|
unset($this->data[$file]);
|
|
|
|
// close the file handle, otherwise we can't delete it on Windows
|
|
$this->closeHandle($file);
|
|
|
|
return F::remove($file);
|
|
}
|
|
|
|
$yaml = Data::encode($this->data[$file], 'yaml');
|
|
|
|
// delete all file contents first
|
|
if (rewind($handle) !== true || ftruncate($handle, 0) !== true) {
|
|
throw new Exception('Could not write lock file ' . $file . '.'); // @codeCoverageIgnore
|
|
}
|
|
|
|
// write the new contents
|
|
$result = fwrite($handle, $yaml);
|
|
if (is_int($result) === false || $result === 0) {
|
|
throw new Exception('Could not write lock file ' . $file . '.'); // @codeCoverageIgnore
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|