Initial commit
This commit is contained in:
commit
efa5624dab
687 changed files with 162710 additions and 0 deletions
331
kirby/src/Content/PlainTextStorage.php
Normal file
331
kirby/src/Content/PlainTextStorage.php
Normal file
|
|
@ -0,0 +1,331 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Content;
|
||||
|
||||
use Kirby\Cms\File;
|
||||
use Kirby\Cms\Language;
|
||||
use Kirby\Cms\Page;
|
||||
use Kirby\Cms\Site;
|
||||
use Kirby\Cms\User;
|
||||
use Kirby\Data\Data;
|
||||
use Kirby\Exception\Exception;
|
||||
use Kirby\Exception\LogicException;
|
||||
use Kirby\Filesystem\Dir;
|
||||
use Kirby\Filesystem\F;
|
||||
|
||||
/**
|
||||
* Content storage handler using plain text files
|
||||
* stored in the content folder
|
||||
*
|
||||
* @package Kirby Content
|
||||
* @author Lukas Bestle <lukas@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://getkirby.com/license
|
||||
* @since 4.0.0
|
||||
* @unstable
|
||||
*/
|
||||
class PlainTextStorage extends Storage
|
||||
{
|
||||
/**
|
||||
* Creates the absolute directory path for the model
|
||||
*/
|
||||
protected function contentDirectory(VersionId $versionId): string
|
||||
{
|
||||
$directory = match (true) {
|
||||
$this->model instanceof File
|
||||
=> dirname($this->model->root()),
|
||||
default
|
||||
=> $this->model->root()
|
||||
};
|
||||
|
||||
if ($versionId->is('changes')) {
|
||||
$directory .= '/_changes';
|
||||
}
|
||||
|
||||
return $directory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute path to the content file
|
||||
* @internal To be made `protected` when the CMS core no longer relies on it
|
||||
*/
|
||||
public function contentFile(VersionId $versionId, Language $language): string
|
||||
{
|
||||
// get the filename without extension and language code
|
||||
return match (true) {
|
||||
$this->model instanceof File => $this->contentFileForFile($this->model, $versionId, $language),
|
||||
$this->model instanceof Page => $this->contentFileForPage($this->model, $versionId, $language),
|
||||
$this->model instanceof Site => $this->contentFileForSite($this->model, $versionId, $language),
|
||||
$this->model instanceof User => $this->contentFileForUser($this->model, $versionId, $language),
|
||||
// @codeCoverageIgnoreStart
|
||||
default => throw new LogicException(
|
||||
message: 'Cannot determine content file for model type "' . $this->model::CLASS_ALIAS . '"'
|
||||
)
|
||||
// @codeCoverageIgnoreEnd
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute path to the content file of a file model
|
||||
*/
|
||||
protected function contentFileForFile(File $model, VersionId $versionId, Language $language): string
|
||||
{
|
||||
return $this->contentDirectory($versionId) . '/' . $this->contentFilename($model->filename(), $language);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute path to the content file of a page model
|
||||
*/
|
||||
protected function contentFileForPage(Page $model, VersionId $versionId, Language $language): string
|
||||
{
|
||||
return $this->contentDirectory($versionId) . '/' . $this->contentFilename($model->intendedTemplate()->name(), $language);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute path to the content file of a site model
|
||||
*/
|
||||
protected function contentFileForSite(Site $model, VersionId $versionId, Language $language): string
|
||||
{
|
||||
return $this->contentDirectory($versionId) . '/' . $this->contentFilename('site', $language);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute path to the content file of a user model
|
||||
*/
|
||||
protected function contentFileForUser(User $model, VersionId $versionId, Language $language): string
|
||||
{
|
||||
return $this->contentDirectory($versionId) . '/' . $this->contentFilename('user', $language);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a filename with extension and optional language code
|
||||
* in a multi-language installation
|
||||
*/
|
||||
protected function contentFilename(string $name, Language $language): string
|
||||
{
|
||||
$kirby = $this->model->kirby();
|
||||
$extension = $kirby->contentExtension();
|
||||
|
||||
if ($language->isSingle() === false) {
|
||||
return $name . '.' . $language->code() . '.' . $extension;
|
||||
}
|
||||
|
||||
return $name . '.' . $extension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array with content files of all languages
|
||||
* @internal To be made `protected` when the CMS core no longer relies on it
|
||||
*/
|
||||
public function contentFiles(VersionId $versionId): array
|
||||
{
|
||||
if ($this->model->kirby()->multilang() === true) {
|
||||
return $this->model->kirby()->languages()->values(
|
||||
fn ($language) => $this->contentFile($versionId, $language)
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
$this->contentFile($versionId, Language::single())
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an existing version in an idempotent way if it was already deleted
|
||||
*/
|
||||
public function delete(VersionId $versionId, Language $language): void
|
||||
{
|
||||
$contentFile = $this->contentFile($versionId, $language);
|
||||
|
||||
// @codeCoverageIgnoreStart
|
||||
if (F::unlink($contentFile) !== true) {
|
||||
throw new Exception(message: 'Could not delete content file');
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
$contentDirectory = $this->contentDirectory($versionId);
|
||||
|
||||
// clean up empty content directories (_changes or the page/user directory)
|
||||
$this->deleteEmptyDirectory($contentDirectory);
|
||||
|
||||
// delete empty _drafts directories for pages
|
||||
if (
|
||||
$versionId->is('latest') === true &&
|
||||
$this->model instanceof Page &&
|
||||
$this->model->isDraft() === true
|
||||
) {
|
||||
$this->deleteEmptyDirectory(dirname($contentDirectory));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to delete empty _changes directories
|
||||
*
|
||||
* @throws \Kirby\Exception\Exception if the directory cannot be deleted
|
||||
*/
|
||||
protected function deleteEmptyDirectory(string $directory): void
|
||||
{
|
||||
if (
|
||||
Dir::exists($directory) === true &&
|
||||
Dir::isEmpty($directory) === true
|
||||
) {
|
||||
// @codeCoverageIgnoreStart
|
||||
if (Dir::remove($directory) !== true) {
|
||||
throw new Exception(
|
||||
message: 'Could not delete empty content directory'
|
||||
);
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a version exists
|
||||
*/
|
||||
public function exists(VersionId $versionId, Language $language): bool
|
||||
{
|
||||
$contentFile = $this->contentFile($versionId, $language);
|
||||
|
||||
// The version definitely exists, if there's a
|
||||
// matching content file
|
||||
if (file_exists($contentFile) === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// A changed version or non-default language version does not exist
|
||||
// if the content file was not found
|
||||
if (
|
||||
$versionId->is('latest') === false ||
|
||||
$language->isDefault() === false
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Whether the default version exists,
|
||||
// depends on different cases for each model.
|
||||
// Page, Site and User exist as soon as the folder is there.
|
||||
// A File exists as soon as the file is there.
|
||||
return match (true) {
|
||||
$this->model instanceof File => is_file($this->model->root()) === true,
|
||||
$this->model instanceof Page,
|
||||
$this->model instanceof Site,
|
||||
$this->model instanceof User => is_dir($this->model->root()) === true,
|
||||
// @codeCoverageIgnoreStart
|
||||
default => throw new LogicException(
|
||||
message: 'Cannot determine existence for model type "' . $this->model::CLASS_ALIAS . '"'
|
||||
)
|
||||
// @codeCoverageIgnoreEnd
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two version-language-storage combinations
|
||||
*/
|
||||
public function isSameStorageLocation(
|
||||
VersionId $fromVersionId,
|
||||
Language $fromLanguage,
|
||||
VersionId|null $toVersionId = null,
|
||||
Language|null $toLanguage = null,
|
||||
Storage|null $toStorage = null
|
||||
) {
|
||||
// fallbacks to allow keeping the method call lean
|
||||
$toVersionId ??= $fromVersionId;
|
||||
$toLanguage ??= $fromLanguage;
|
||||
$toStorage ??= $this;
|
||||
|
||||
// no need to compare content files if the new
|
||||
// storage type is different
|
||||
if ($toStorage instanceof self === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$contentFileA = $this->contentFile($fromVersionId, $fromLanguage);
|
||||
$contentFileB = $toStorage->contentFile($toVersionId, $toLanguage);
|
||||
|
||||
return $contentFileA === $contentFileB;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the modification timestamp of a version
|
||||
* if it exists
|
||||
*/
|
||||
public function modified(VersionId $versionId, Language $language): int|null
|
||||
{
|
||||
$modified = F::modified($this->contentFile($versionId, $language));
|
||||
|
||||
if (is_int($modified) === true) {
|
||||
return $modified;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the stored content fields
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function read(VersionId $versionId, Language $language): array
|
||||
{
|
||||
$contentFile = $this->contentFile($versionId, $language);
|
||||
|
||||
if (file_exists($contentFile) === true) {
|
||||
return Data::read($contentFile);
|
||||
}
|
||||
|
||||
// For existing versions that don't have a content file yet,
|
||||
// we can safely return an empty array that can be filled later.
|
||||
// This might be the case for pages that only have a directory
|
||||
// so far, or for files that don't have any metadata yet.
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the modification timestamp of an existing version
|
||||
*
|
||||
* @throws \Kirby\Exception\Exception If the file cannot be touched
|
||||
*/
|
||||
public function touch(VersionId $versionId, Language $language): void
|
||||
{
|
||||
$success = touch($this->contentFile($versionId, $language));
|
||||
|
||||
// @codeCoverageIgnoreStart
|
||||
if ($success !== true) {
|
||||
throw new Exception(
|
||||
message: 'Could not touch existing content file'
|
||||
);
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the content fields of an existing version
|
||||
*
|
||||
* @param array<string, string> $fields Content fields
|
||||
*
|
||||
* @throws \Kirby\Exception\Exception If the content cannot be written
|
||||
*/
|
||||
protected function write(VersionId $versionId, Language $language, array $fields): void
|
||||
{
|
||||
// only store non-null value fields
|
||||
$fields = array_filter($fields, fn ($field) => $field !== null);
|
||||
|
||||
// Content for files is only stored when there are any fields.
|
||||
// Otherwise, the storage handler will take care here of cleaning up
|
||||
// unnecessary content files.
|
||||
if ($this->model instanceof File && $fields === []) {
|
||||
$this->delete($versionId, $language);
|
||||
return;
|
||||
}
|
||||
|
||||
$success = Data::write($this->contentFile($versionId, $language), $fields);
|
||||
|
||||
// @codeCoverageIgnoreStart
|
||||
if ($success !== true) {
|
||||
throw new Exception(message: 'Could not write the content file');
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue