diff --git a/panel/src/components/Views/PreviewView.vue b/panel/src/components/Views/PreviewView.vue
index a20c4e4fdf..27df5f55aa 100644
--- a/panel/src/components/Views/PreviewView.vue
+++ b/panel/src/components/Views/PreviewView.vue
@@ -91,10 +91,16 @@
- {{ $t("lock.unsaved.empty") }}
-
- {{ $t("edit") }}
-
+
+ This content is locked by our old lock system.
+ Changes cannot be previewed.
+
+
+ {{ $t("lock.unsaved.empty") }}
+
+ {{ $t("edit") }}
+
+
@@ -223,6 +229,8 @@ export default {
flex-grow: 1;
justify-content: center;
flex-direction: column;
+ text-align: center;
+ padding-inline: var(--spacing-3);
gap: var(--spacing-6);
--button-color-text: var(--color-text);
}
diff --git a/src/Api/Controller/Changes.php b/src/Api/Controller/Changes.php
index 7ebe9cdf68..99e207ca88 100644
--- a/src/Api/Controller/Changes.php
+++ b/src/Api/Controller/Changes.php
@@ -3,7 +3,9 @@
namespace Kirby\Api\Controller;
use Kirby\Cms\ModelWithContent;
+use Kirby\Content\Lock;
use Kirby\Content\VersionId;
+use Kirby\Filesystem\F;
use Kirby\Form\Form;
/**
@@ -18,6 +20,20 @@
*/
class Changes
{
+ /**
+ * Cleans up legacy lock files. The `discard`, `publish` and `save` actions
+ * are perfect for this cleanup job. They will be stopped early if
+ * the lock is still active and otherwise, we can use them to clean
+ * up outdated .lock files to keep the content folders clean. This
+ * can be removed as soon as old .lock files should no longer be around.
+ *
+ * @todo Remove in 6.0.0
+ */
+ protected static function cleanup(ModelWithContent $model): void
+ {
+ F::remove(Lock::legacyFile($model));
+ }
+
/**
* Discards unsaved changes by deleting the changes version
*/
@@ -25,6 +41,10 @@ public static function discard(ModelWithContent $model): array
{
$model->version(VersionId::changes())->delete('current');
+ // Removes the old .lock file when it is no longer needed
+ // @todo Remove in 6.0.0
+ static::cleanup($model);
+
return [
'status' => 'ok'
];
@@ -41,6 +61,10 @@ public static function publish(ModelWithContent $model, array $input): array
input: $input
);
+ // Removes the old .lock file when it is no longer needed
+ // @todo Remove in 6.0.0
+ static::cleanup($model);
+
// get the changes version
$changes = $model->version(VersionId::changes());
@@ -77,6 +101,10 @@ public static function save(ModelWithContent $model, array $input): array
$changes = $model->version(VersionId::changes());
$latest = $model->version(VersionId::latest());
+ // Removes the old .lock file when it is no longer needed
+ // @todo Remove in 6.0.0
+ static::cleanup($model);
+
// combine the new field changes with the
// last published state
$changes->save(
diff --git a/src/Content/Lock.php b/src/Content/Lock.php
index c0de6d74fa..5d70c26c15 100644
--- a/src/Content/Lock.php
+++ b/src/Content/Lock.php
@@ -5,7 +5,9 @@
use Kirby\Cms\App;
use Kirby\Cms\Language;
use Kirby\Cms\Languages;
+use Kirby\Cms\ModelWithContent;
use Kirby\Cms\User;
+use Kirby\Data\Data;
use Kirby\Toolkit\Str;
/**
@@ -27,6 +29,7 @@ class Lock
public function __construct(
protected User|null $user = null,
protected int|null $modified = null,
+ protected bool $legacy = false
) {
}
@@ -39,6 +42,11 @@ public static function for(
Version $version,
Language|string $language = 'default'
): static {
+
+ if ($legacy = static::legacy($version->model())) {
+ return $legacy;
+ }
+
// wildcard to search for a lock in any language
// the first locked one will be preferred
if ($language === '*') {
@@ -87,13 +95,21 @@ public function isActive(): bool
}
/**
- * Check if content locking is enabled at all
+ * Checks if content locking is enabled at all
*/
public static function isEnabled(): bool
{
return App::instance()->option('content.locking', true) !== false;
}
+ /**
+ * Checks if the lock is coming from an old .lock file
+ */
+ public function isLegacy(): bool
+ {
+ return $this->legacy;
+ }
+
/**
* Checks if the lock is actually locked
*/
@@ -124,6 +140,52 @@ public function isLocked(): bool
return true;
}
+ /**
+ * Looks for old .lock files and tries to create a
+ * usable lock instance from them
+ */
+ public static function legacy(ModelWithContent $model): static|null
+ {
+ $kirby = $model->kirby();
+ $file = static::legacyFile($model);
+ $id = '/' . $model->id();
+
+ // no legacy lock file? no lock.
+ if (file_exists($file) === false) {
+ return null;
+ }
+
+ $data = Data::read($file, 'yml', fail: false)[$id] ?? [];
+
+ // no valid lock entry? no lock.
+ if (isset($data['lock']) === false) {
+ return null;
+ }
+
+ // has the lock been unlocked? no lock.
+ if (isset($data['unlock']) === true) {
+ return null;
+ }
+
+ return new static(
+ user: $kirby->user($data['lock']['user']),
+ modified: $data['lock']['time'],
+ legacy: true
+ );
+ }
+
+ /**
+ * Returns the absolute path to a legacy lock file
+ */
+ public static function legacyFile(ModelWithContent $model): string
+ {
+ $root = match ($model::CLASS_ALIAS) {
+ 'file' => dirname($model->root()),
+ default => $model->root()
+ };
+ return $root . '/.lock';
+ }
+
/**
* Returns the timestamp when the locked content has
* been updated. You can pass a format to get a useful,
@@ -147,6 +209,7 @@ public function modified(
public function toArray(): array
{
return [
+ 'isLegacy' => $this->isLegacy(),
'isLocked' => $this->isLocked(),
'modified' => $this->modified('c'),
'user' => [
diff --git a/tests/Content/LockTest.php b/tests/Content/LockTest.php
index 0055d3cbd1..d4871a87f4 100644
--- a/tests/Content/LockTest.php
+++ b/tests/Content/LockTest.php
@@ -5,6 +5,7 @@
use Kirby\Cms\App;
use Kirby\Cms\Language;
use Kirby\Cms\User;
+use Kirby\Data\Data;
/**
* @coversDefaultClass \Kirby\Content\Lock
@@ -155,6 +156,28 @@ public function testForWithLanguageWildcard()
$this->assertSame('admin', $lock->user()->id());
}
+ /**
+ * @covers ::for
+ */
+ public function testForWithLegacyLock()
+ {
+ $page = $this->app->page('test');
+ $file = $page->root() . '/.lock';
+
+ Data::write($file, [
+ '/' . $page->id() => [
+ 'lock' => [
+ 'user' => 'editor',
+ 'time' => $time = time()
+ ]
+ ]
+ ], 'yml');
+
+ $lock = Lock::for($page->version('changes'));
+ $this->assertInstanceOf(Lock::class, $lock);
+ $this->assertTrue($lock->isLocked());
+ }
+
/**
* @covers ::isActive
*/
@@ -215,6 +238,18 @@ public function testIsEnabledWhenDisabled()
$this->assertFalse(Lock::isEnabled());
}
+ /**
+ * @covers ::isLegacy
+ */
+ public function testIsLegacy()
+ {
+ $lock = new Lock();
+ $this->assertFalse($lock->isLegacy());
+
+ $lock = new Lock(legacy: true);
+ $this->assertTrue($lock->isLegacy());
+ }
+
/**
* @covers ::isLocked
*/
@@ -292,6 +327,102 @@ public function testIsLockedWithDifferentUserAndOldTimestamp()
$this->assertFalse($lock->isLocked());
}
+ /**
+ * @covers ::legacy
+ */
+ public function testLegacy()
+ {
+ $page = $this->app->page('test');
+ $file = $page->root() . '/.lock';
+
+ Data::write($file, [
+ '/' . $page->id() => [
+ 'lock' => [
+ 'user' => 'editor',
+ 'time' => $time = time()
+ ]
+ ]
+ ], 'yml');
+
+ $lock = Lock::legacy($page);
+
+ $this->assertInstanceOf(Lock::class, $lock);
+ $this->assertTrue($lock->isLocked());
+ $this->assertTrue($lock->isLegacy());
+ $this->assertSame($this->app->user('editor'), $lock->user());
+ $this->assertSame($time, $lock->modified());
+ }
+
+ /**
+ * @covers ::legacy
+ */
+ public function testLegacyWithoutLockInfo()
+ {
+ $page = $this->app->page('test');
+ $file = $page->root() . '/.lock';
+
+ Data::write($file, [], 'yml');
+
+ $lock = Lock::legacy($page);
+ $this->assertNull($lock);
+ }
+
+ /**
+ * @covers ::legacy
+ */
+ public function testLegacyWithOutdatedFile()
+ {
+ $page = $this->app->page('test');
+ $file = $page->root() . '/.lock';
+
+ Data::write($file, [
+ '/' . $page->id() => [
+ 'lock' => [
+ 'user' => 'editor',
+ 'time' => time() - 60 * 60 * 24
+ ],
+ ]
+ ], 'yml');
+
+ $lock = Lock::legacy($page);
+
+ $this->assertInstanceOf(Lock::class, $lock);
+ $this->assertFalse($lock->isLocked());
+ }
+
+ /**
+ * @covers ::legacy
+ */
+ public function testLegacyWithUnlockedFile()
+ {
+ $page = $this->app->page('test');
+ $file = $page->root() . '/.lock';
+
+ Data::write($file, [
+ '/' . $page->id() => [
+ 'lock' => [
+ 'user' => 'editor',
+ 'time' => time()
+ ],
+ 'unlock' => ['admin']
+ ]
+ ], 'yml');
+
+ $lock = Lock::legacy($page);
+ $this->assertNull($lock);
+ }
+
+ /**
+ * @covers ::legacyFile
+ */
+ public function testLegacyFile()
+ {
+ $page = $this->app->page('test');
+ $expected = $page->root() . '/.lock';
+
+ $this->assertSame($expected, Lock::legacyFile($page));
+ }
+
/**
* @covers ::modified
*/
@@ -319,6 +450,7 @@ public function testToArray()
);
$this->assertSame([
+ 'isLegacy' => false,
'isLocked' => true,
'modified' => date('c', $modified),
'user' => [
diff --git a/tests/Panel/Areas/SiteTest.php b/tests/Panel/Areas/SiteTest.php
index 05957c5989..d2f576f7ba 100644
--- a/tests/Panel/Areas/SiteTest.php
+++ b/tests/Panel/Areas/SiteTest.php
@@ -38,6 +38,7 @@ public function testPage(): void
$this->assertSame('default', $props['blueprint']);
$this->assertSame([
+ 'isLegacy' => false,
'isLocked' => false,
'modified' => null,
'user' => [
@@ -95,6 +96,7 @@ public function testPageFile(): void
$this->assertSame('image', $props['blueprint']);
$this->assertSame([
+ 'isLegacy' => false,
'isLocked' => false,
'modified' => null,
'user' => [
@@ -190,6 +192,7 @@ public function testSiteFile(): void
$this->assertSame('image', $props['blueprint']);
$this->assertSame([
+ 'isLegacy' => false,
'isLocked' => false,
'modified' => null,
'user' => [