diff --git a/src/Content/ContentStorageHandler.php b/src/Content/ContentStorageHandler.php index d6f5219b9a..75bc2ed22d 100644 --- a/src/Content/ContentStorageHandler.php +++ b/src/Content/ContentStorageHandler.php @@ -2,8 +2,10 @@ namespace Kirby\Content; +use Generator; use Kirby\Cms\Language; use Kirby\Cms\ModelWithContent; +use Kirby\Cms\Page; /** * Abstract for content storage handlers; @@ -25,6 +27,26 @@ public function __construct(protected ModelWithContent $model) { } + /** + * Returns generator for all existing version-language combinations + * @todo 5.0.0 Consider more descriptive name and maybe move to a different class + * + * @return Generator<\Kirby\Content\VersionId, \Kirby\Cms\Language> + */ + public function all(): Generator + { + $kirby = $this->model->kirby(); + $languages = $kirby->multilang() === false ? [Language::single()] : $kirby->languages(); + + foreach ($languages as $language) { + foreach ($this->dynamicVersions() as $versionId) { + if ($this->exists($versionId, $language) === true) { + yield $versionId => $language; + } + } + } + } + /** * Creates a new version * @@ -37,6 +59,38 @@ abstract public function create(VersionId $versionId, Language $language, array */ abstract public function delete(VersionId $versionId, Language $language): void; + /** + * Deletes all versions when deleting a language + * @internal + * @todo Move to `Language` class + */ + public function deleteLanguage(Language $language): void + { + foreach ($this->dynamicVersions() as $version) { + $this->delete($version, $language); + } + } + + /** + * Returns all versions available for the model that can be updated + * @internal + * @todo We might want to move this directly to the models later or work + * with a `Versions` class + */ + public function dynamicVersions(): array + { + $versions = [VersionId::changes()]; + + if ( + $this->model instanceof Page === false || + $this->model->isDraft() === false + ) { + $versions[] = VersionId::published(); + } + + return $versions; + } + /** * Checks if a version exists */ @@ -57,6 +111,18 @@ abstract public function move( Language $toLanguage ): void; + /** + * Adapts all versions when converting languages + * @internal + * @todo Move to `Language` class + */ + public function moveLanguage(Language $fromLanguage, Language $toLanguage): void + { + foreach ($this->dynamicVersions() as $versionId) { + $this->move($versionId, $fromLanguage, $versionId, $toLanguage); + } + } + /** * Returns the stored content fields * @@ -73,6 +139,20 @@ abstract public function read(VersionId $versionId, Language $language): array; */ abstract public function touch(VersionId $versionId, Language $language): void; + /** + * Touches all versions of a language + * @internal + * @todo Move to `Language` class + */ + public function touchLanguage(Language $language): void + { + foreach ($this->dynamicVersions() as $version) { + if ($this->exists($version, $language) === true) { + $this->touch($version, $language); + } + } + } + /** * Updates the content fields of an existing version * diff --git a/tests/Content/ContentStorageHandlerTest.php b/tests/Content/ContentStorageHandlerTest.php new file mode 100644 index 0000000000..527529a276 --- /dev/null +++ b/tests/Content/ContentStorageHandlerTest.php @@ -0,0 +1,370 @@ +setUpMultiLanguage(); + + $handler = new TestContentStorageHandler( + new File([ + 'filename' => 'test.jpg', + 'parent' => new Page(['slug' => 'test']) + ]) + ); + + $versions = iterator_to_array($handler->all(), false); + + // The TestContentStorage handler always returns true + // for every version and language. Thus there should be + // 2 versions for every language. + // + // article.en.txt + // article.de.txt + // _changes/article.en.txt + // _changes/article.de.txt + $this->assertCount(4, $versions); + } + + /** + * @covers ::all + */ + public function testAllSingleLanguageForFile() + { + $this->setUpSingleLanguage(); + + $handler = new TestContentStorageHandler( + new File([ + 'filename' => 'test.jpg', + 'parent' => new Page(['slug' => 'test']) + ]) + ); + + $versions = iterator_to_array($handler->all(), false); + + // The TestContentStorage handler always returns true + // for every version and language. Thus there should be + // 2 versions in a single language installation. + // + // article.txt + // _changes/article.txt + $this->assertCount(2, $versions); + } + + /** + * @covers ::all + */ + public function testAllMultiLanguageForPage() + { + $this->setUpMultiLanguage(); + + $handler = new TestContentStorageHandler( + new Page(['slug' => 'test', 'isDraft' => false]) + ); + + $versions = iterator_to_array($handler->all(), false); + + // A page that's not in draft mode can have published and changes versions + // and thus should have changes and published for every language + $this->assertCount(4, $versions); + } + + /** + * @covers ::all + */ + public function testAllMultiLanguageForPageDraft() + { + $this->setUpMultiLanguage(); + + $handler = new TestContentStorageHandler( + new Page(['slug' => 'test', 'isDraft' => true]) + ); + + $versions = iterator_to_array($handler->all(), false); + + // A draft page has only changes and thus should only have + // a changes for every language, but no published versions + $this->assertCount(2, $versions); + } + + /** + * @covers ::all + */ + public function testAllSingleLanguageForPage() + { + $this->setUpSingleLanguage(); + + $handler = new TestContentStorageHandler( + new Page(['slug' => 'test', 'isDraft' => false]) + ); + + $versions = iterator_to_array($handler->all(), false); + + // A page that's not in draft mode can have published and changes versions + $this->assertCount(2, $versions); + } + + /** + * @covers ::all + */ + public function testAllSingleLanguageForPageDraft() + { + $this->setUpSingleLanguage(); + + $handler = new TestContentStorageHandler( + new Page(['slug' => 'test', 'isDraft' => true]) + ); + + $versions = iterator_to_array($handler->all(), false); + + // A draft page has only changes and thus should only have + // a single version in a single language installation + $this->assertCount(1, $versions); + } + + /** + * @covers ::deleteLanguage + */ + public function testDeleteLanguageMultiLanguage() + { + $this->setUpMultiLanguage(); + + // Use the plain text handler, as the abstract class and the test handler do not + // implement the necessary methods to test this. + $handler = new PlainTextContentStorageHandler( + model: $this->model + ); + + Data::write($filePublished = $this->model->root() . '/article.de.txt', []); + Data::write($fileChanges = $this->model->root() . '/_changes/article.de.txt', []); + + $this->assertFileExists($filePublished); + $this->assertFileExists($fileChanges); + + $handler->deleteLanguage($this->app->language('de')); + + $this->assertFileDoesNotExist($filePublished); + $this->assertFileDoesNotExist($fileChanges); + } + + /** + * @covers ::deleteLanguage + */ + public function testDeleteLanguageSingleLanguage() + { + $this->setUpSingleLanguage(); + + // Use the plain text handler, as the abstract class and the test handler do not + // implement the necessary methods to test this. + $handler = new PlainTextContentStorageHandler( + model: $this->model + ); + + Data::write($filePublished = $this->model->root() . '/article.txt', []); + Data::write($fileChanges = $this->model->root() . '/_changes/article.txt', []); + + $this->assertFileExists($filePublished); + $this->assertFileExists($fileChanges); + + $handler->deleteLanguage(Language::single()); + + $this->assertFileDoesNotExist($filePublished); + $this->assertFileDoesNotExist($fileChanges); + } + + /** + * @covers ::dynamicVersions + */ + public function testDynamicVersionsForFile() + { + $handler = new TestContentStorageHandler( + new File([ + 'filename' => 'test.jpg', + 'parent' => new Page(['slug' => 'test']) + ]) + ); + + $versions = $handler->dynamicVersions(); + + $this->assertCount(2, $versions); + $this->assertSame(VersionId::CHANGES, $versions[0]->value()); + $this->assertSame(VersionId::PUBLISHED, $versions[1]->value()); + } + + /** + * @covers ::dynamicVersions + */ + public function testDynamicVersionsForPage() + { + $handler = new TestContentStorageHandler( + new Page(['slug' => 'test', 'isDraft' => false]) + ); + + $versions = $handler->dynamicVersions(); + + $this->assertCount(2, $versions); + $this->assertSame(VersionId::CHANGES, $versions[0]->value()); + $this->assertSame(VersionId::PUBLISHED, $versions[1]->value()); + } + + /** + * @covers ::dynamicVersions + */ + public function testDynamicVersionsForPageDraft() + { + $handler = new TestContentStorageHandler( + new Page(['slug' => 'test', 'isDraft' => true]) + ); + + $versions = $handler->dynamicVersions(); + + $this->assertCount(1, $versions); + $this->assertSame(VersionId::CHANGES, $versions[0]->value()); + } + + /** + * @covers ::dynamicVersions + */ + public function testDynamicVersionsForSite() + { + $handler = new TestContentStorageHandler( + new Site() + ); + + $versions = $handler->dynamicVersions(); + + $this->assertCount(2, $versions); + $this->assertSame(VersionId::CHANGES, $versions[0]->value()); + $this->assertSame(VersionId::PUBLISHED, $versions[1]->value()); + } + + /** + * @covers ::dynamicVersions + */ + public function testDynamicVersionsForUser() + { + $handler = new TestContentStorageHandler( + new User(['email' => 'test@getkirby.com']) + ); + + $versions = $handler->dynamicVersions(); + + $this->assertCount(2, $versions); + $this->assertSame(VersionId::CHANGES, $versions[0]->value()); + $this->assertSame(VersionId::PUBLISHED, $versions[1]->value()); + } + + /** + * @covers ::moveLanguage + */ + public function testMoveSingleLanguageToMultiLanguage() + { + $this->setUpMultiLanguage(); + + // Use the plain text handler, as the abstract class and the test handler do not + // implement the necessary methods to test this. + $handler = new PlainTextContentStorageHandler( + model: $this->model + ); + + Data::write($filePublished = $this->model->root() . '/article.txt', []); + Data::write($fileChanges = $this->model->root() . '/_changes/article.txt', []); + + $this->assertFileExists($filePublished); + $this->assertFileExists($fileChanges); + + $handler->moveLanguage( + Language::single(), + $this->app->language('en') + ); + + $this->assertFileDoesNotExist($filePublished); + $this->assertFileDoesNotExist($fileChanges); + + $this->assertFileExists($this->model->root() . '/article.en.txt'); + $this->assertFileExists($this->model->root() . '/_changes/article.en.txt'); + } + + /** + * @covers ::moveLanguage + */ + public function testMoveMultiLanguageToSingleLanguage() + { + $this->setUpMultiLanguage(); + + // Use the plain text handler, as the abstract class and the test handler do not + // implement the necessary methods to test this. + $handler = new PlainTextContentStorageHandler( + model: $this->model + ); + + Data::write($filePublished = $this->model->root() . '/article.en.txt', []); + Data::write($fileChanges = $this->model->root() . '/_changes/article.en.txt', []); + + $this->assertFileExists($filePublished); + $this->assertFileExists($fileChanges); + + $handler->moveLanguage( + $this->app->language('en'), + Language::single(), + ); + + $this->assertFileDoesNotExist($filePublished); + $this->assertFileDoesNotExist($fileChanges); + + $this->assertFileExists($this->model->root() . '/article.txt'); + $this->assertFileExists($this->model->root() . '/_changes/article.txt'); + } + + /** + * @covers ::touchLanguage + */ + public function testTouchLanguageMultiLanguage() + { + $this->setUpMultiLanguage(); + + // Use the plain text handler, as the abstract class and the test handler do not + // implement the necessary methods to test this. + $handler = new PlainTextContentStorageHandler( + model: $this->model + ); + + Dir::make($this->model->root()); + Dir::make($this->model->root() . '/_changes'); + + touch($filePublished = $this->model->root() . '/article.de.txt', 123456); + touch($fileChanges = $this->model->root() . '/_changes/article.de.txt', 123456); + + $this->assertSame(123456, filemtime($filePublished)); + $this->assertSame(123456, filemtime($fileChanges)); + + $minTime = time(); + + $handler->touchLanguage($this->app->language('de')); + + clearstatcache(); + + $this->assertGreaterThanOrEqual($minTime, filemtime($fileChanges)); + $this->assertGreaterThanOrEqual($minTime, filemtime($filePublished)); + } + +} diff --git a/tests/Content/PlainTextContentStorageHandlerTest.php b/tests/Content/PlainTextContentStorageHandlerTest.php index 90f1c822ef..078023baa6 100644 --- a/tests/Content/PlainTextContentStorageHandlerTest.php +++ b/tests/Content/PlainTextContentStorageHandlerTest.php @@ -2,7 +2,6 @@ namespace Kirby\Content; -use Kirby\Cms\App; use Kirby\Cms\File; use Kirby\Cms\Language; use Kirby\Cms\Page; @@ -10,7 +9,6 @@ use Kirby\Data\Data; use Kirby\Exception\LogicException; use Kirby\Filesystem\Dir; -use Kirby\TestCase; /** * @coversDefaultClass Kirby\Content\PlainTextContentStorageHandler @@ -20,71 +18,20 @@ class PlainTextContentStorageHandlerTest extends TestCase { public const TMP = KIRBY_TMP_DIR . '/Content.PlainTextContentStorage'; - protected $model; protected $storage; - public function setUp(): void - { - Dir::make(static::TMP); - } - public function setUpMultiLanguage(): void { - $this->app = new App([ - 'languages' => [ - [ - 'code' => 'en', - 'default' => true - ], - [ - 'code' => 'de' - ] - ], - 'roots' => [ - 'index' => static::TMP - ], - 'site' => [ - 'children' => [ - [ - 'slug' => 'a-page', - 'template' => 'article', - ] - ] - ] - ]); + parent::setUpMultiLanguage(); - $this->model = $this->app->page('a-page'); $this->storage = new PlainTextContentStorageHandler($this->model); - - Dir::make($this->model->root()); } public function setUpSingleLanguage(): void { - $this->app = new App([ - 'roots' => [ - 'index' => static::TMP - ], - 'site' => [ - 'children' => [ - [ - 'slug' => 'a-page', - 'template' => 'article' - ] - ] - ] - ]); + parent::setUpSingleLanguage(); - $this->model = $this->app->page('a-page'); $this->storage = new PlainTextContentStorageHandler($this->model); - - Dir::make($this->model->root()); - } - - public function tearDown(): void - { - App::destroy(); - Dir::remove(static::TMP); } /** diff --git a/tests/Content/TestCase.php b/tests/Content/TestCase.php new file mode 100644 index 0000000000..cd0e11a39d --- /dev/null +++ b/tests/Content/TestCase.php @@ -0,0 +1,76 @@ +app = new App([ + 'languages' => [ + [ + 'code' => 'en', + 'default' => true + ], + [ + 'code' => 'de' + ] + ], + 'roots' => [ + 'index' => static::TMP + ], + 'site' => [ + 'children' => [ + [ + 'slug' => 'a-page', + 'template' => 'article', + ] + ] + ] + ]); + + $this->model = $this->app->page('a-page'); + + Dir::make($this->model->root()); + } + + public function setUpSingleLanguage(): void + { + $this->app = new App([ + 'roots' => [ + 'index' => static::TMP + ], + 'site' => [ + 'children' => [ + [ + 'slug' => 'a-page', + 'template' => 'article' + ] + ] + ] + ]); + + $this->model = $this->app->page('a-page'); + + Dir::make($this->model->root()); + } + + public function tearDown(): void + { + App::destroy(); + Dir::remove(static::TMP); + } +} diff --git a/tests/Content/TestContentStorageHandler.php b/tests/Content/TestContentStorageHandler.php new file mode 100644 index 0000000000..363cf13315 --- /dev/null +++ b/tests/Content/TestContentStorageHandler.php @@ -0,0 +1,47 @@ +assertFileExists($this->contentFile($language, $versionId)); @@ -42,68 +37,6 @@ public function contentFile(string|null $language = null, VersionId|null $versio '.txt'; } - public function setUp(): void - { - Dir::make(static::TMP); - } - - public function setUpMultiLanguage(): void - { - $this->app = new App([ - 'languages' => [ - [ - 'code' => 'en', - 'default' => true - ], - [ - 'code' => 'de' - ] - ], - 'roots' => [ - 'index' => static::TMP - ], - 'site' => [ - 'children' => [ - [ - 'slug' => 'a-page', - 'template' => 'article', - ] - ] - ] - ]); - - $this->model = $this->app->page('a-page'); - - Dir::make($this->model->root()); - } - - public function setUpSingleLanguage(): void - { - $this->app = new App([ - 'roots' => [ - 'index' => static::TMP - ], - 'site' => [ - 'children' => [ - [ - 'slug' => 'a-page', - 'template' => 'article' - ] - ] - ] - ]); - - $this->model = $this->app->page('a-page'); - - Dir::make($this->model->root()); - } - - public function tearDown(): void - { - App::destroy(); - Dir::remove(static::TMP); - } - /** * @covers ::content * @covers ::language