Upgrade to 4.2.0

This commit is contained in:
Bastian Allgeier
2024-04-10 11:09:52 +02:00
parent 77d9337371
commit 7f4eb7509d
88 changed files with 1187 additions and 490 deletions

View File

@@ -71,7 +71,7 @@ class Api extends BaseApi
$field = Form::for($model)->field($name);
$fieldApi = $this->clone([
'data' => array_merge($this->data(), ['field' => $field]),
'data' => [...$this->data(), 'field' => $field],
'routes' => $field->api(),
]);
@@ -185,6 +185,30 @@ class Api extends BaseApi
return $pages->query($this->requestBody());
}
/**
* @throws \Kirby\Exception\NotFoundException if the section type cannot be found or the section cannot be loaded
*/
public function sectionApi(
ModelWithContent $model,
string $name,
string|null $path = null
): mixed {
if (!$section = $model->blueprint()?->section($name)) {
throw new NotFoundException('The section "' . $name . '" could not be found');
}
$sectionApi = $this->clone([
'data' => [...$this->data(), 'section' => $section],
'routes' => $section->api(),
]);
return $sectionApi->call(
$path,
$this->requestMethod(),
$this->requestData()
);
}
/**
* Returns the current Session instance
*

View File

@@ -659,6 +659,11 @@ trait AppPlugins
*/
protected function extensionsFromSystem(): void
{
// Always start with fresh fields and sections
// from the core and add plugins on top of that
FormField::$types = [];
Section::$types = [];
// mixins
FormField::$mixins = $this->core->fieldMixins();
Section::$mixins = $this->core->sectionMixins();
@@ -674,8 +679,8 @@ trait AppPlugins
$this->extendCacheTypes($this->core->cacheTypes());
$this->extendComponents($this->core->components());
$this->extendBlueprints($this->core->blueprints());
$this->extendFields($this->core->fields());
$this->extendFieldMethods($this->core->fieldMethods());
$this->extendFields($this->core->fields());
$this->extendSections($this->core->sections());
$this->extendSnippets($this->core->snippets());
$this->extendTags($this->core->kirbyTags());

View File

@@ -34,6 +34,8 @@ class Blueprint
protected $sections = [];
protected $tabs = [];
protected array|null $fileTemplates = null;
/**
* Magic getter/caller for any blueprint prop
*/
@@ -96,6 +98,115 @@ class Blueprint
return $this->props ?? [];
}
/**
* Gathers what file templates are allowed in
* this model based on the blueprint
*/
public function acceptedFileTemplates(string $inSection = null): array
{
// get cached results for the current file model
// (except when collecting for a specific section)
if ($inSection === null && $this->fileTemplates !== null) {
return $this->fileTemplates; // @codeCoverageIgnore
}
$templates = [];
// collect all allowed file templates from blueprint…
foreach ($this->sections() as $section) {
// if collecting for a specific section, skip all others
if ($inSection !== null && $section->name() !== $inSection) {
continue;
}
$templates = match ($section->type()) {
'files' => [...$templates, $section->template() ?? 'default'],
'fields' => [
...$templates,
...$this->acceptedFileTemplatesFromFields($section->fields())
],
default => $templates
};
}
// no caching for when collecting for specific section
if ($inSection !== null) {
return $templates; // @codeCoverageIgnore
}
return $this->fileTemplates = $templates;
}
/**
* Gathers the allowed file templates from model's fields
*/
protected function acceptedFileTemplatesFromFields(array $fields): array
{
$templates = [];
foreach ($fields as $field) {
// fields with uploads settings
if (isset($field['uploads']) === true && is_array($field['uploads']) === true) {
$templates = [
...$templates,
...$this->acceptedFileTemplatesFromFieldUploads($field['uploads'])
];
continue;
}
// structure and object fields
if (isset($field['fields']) === true && is_array($field['fields']) === true) {
$templates = [
...$templates,
...$this->acceptedFileTemplatesFromFields($field['fields']),
];
continue;
}
// layout and blocks fields
if (isset($field['fieldsets']) === true && is_array($field['fieldsets']) === true) {
$templates = [
...$templates,
...$this->acceptedFileTemplatesFromFieldsets($field['fieldsets'])
];
continue;
}
}
return $templates;
}
/**
* Gathers the allowed file templates from fieldsets
*/
protected function acceptedFileTemplatesFromFieldsets(array $fieldsets): array
{
$templates = [];
foreach ($fieldsets as $fieldset) {
foreach (($fieldset['tabs'] ?? []) as $tab) {
$templates = array_merge($templates, $this->acceptedFileTemplatesFromFields($tab['fields'] ?? []));
}
}
return $templates;
}
/**
* Extracts templates from field uploads settings
*/
protected function acceptedFileTemplatesFromFieldUploads(array $uploads): array
{
// only if the `uploads` parent is this model
if ($target = $uploads['parent'] ?? null) {
if ($this->model->id() !== $target) {
return [];
}
}
return [($uploads['template'] ?? 'default')];
}
/**
* Converts all column definitions, that
* are not wrapped in a tab, into a generic tab
@@ -186,24 +297,20 @@ class Blueprint
];
}
$extends = $props['extends'] ?? null;
if ($extends === null) {
return $props;
}
foreach (A::wrap($extends) as $extend) {
try {
$mixin = static::find($extend);
$mixin = static::extend($mixin);
$props = A::merge($mixin, $props, A::MERGE_REPLACE);
} catch (Exception) {
// keep the props unextended if the snippet wasn't found
if ($extends = $props['extends'] ?? null) {
foreach (A::wrap($extends) as $extend) {
try {
$mixin = static::find($extend);
$mixin = static::extend($mixin);
$props = A::merge($mixin, $props, A::MERGE_REPLACE);
} catch (Exception) {
// keep the props unextended if the snippet wasn't found
}
}
}
// remove the extends flag
unset($props['extends']);
// remove the extends flag
unset($props['extends']);
}
return $props;
}
@@ -280,6 +387,7 @@ class Blueprint
if (is_string($file) === true && F::exists($file) === true) {
return static::$loaded[$name] = Data::read($file);
}
if (is_array($file) === true) {
return static::$loaded[$name] = $file;
}
@@ -354,7 +462,10 @@ class Blueprint
continue;
}
$columnProps = $this->convertFieldsToSections($tabName . '-col-' . $columnKey, $columnProps);
$columnProps = $this->convertFieldsToSections(
$tabName . '-col-' . $columnKey,
$columnProps
);
// inject getting started info, if the sections are empty
if (empty($columnProps['sections']) === true) {
@@ -367,10 +478,14 @@ class Blueprint
];
}
$columns[$columnKey] = array_merge($columnProps, [
$columns[$columnKey] = [
...$columnProps,
'width' => $columnProps['width'] ?? '1/1',
'sections' => $this->normalizeSections($tabName, $columnProps['sections'] ?? [])
]);
'sections' => $this->normalizeSections(
$tabName,
$columnProps['sections'] ?? []
)
];
}
return $columns;
@@ -390,10 +505,9 @@ class Blueprint
/**
* Normalize field props for a single field
*
* @param array|string $props
* @throws \Kirby\Exception\InvalidArgumentException If the filed name is missing or the field type is invalid
*/
public static function fieldProps($props): array
public static function fieldProps(array|string $props): array
{
$props = static::extend($props);
@@ -432,12 +546,13 @@ class Blueprint
}
// add some useful defaults
return array_merge($props, [
return [
...$props,
'label' => $props['label'] ?? ucfirst($name),
'name' => $name,
'type' => $type,
'width' => $props['width'] ?? '1/1',
]);
];
}
/**
@@ -496,11 +611,16 @@ class Blueprint
// resolve field groups
if ($fieldProps['type'] === 'group') {
if (empty($fieldProps['fields']) === false && is_array($fieldProps['fields']) === true) {
if (
empty($fieldProps['fields']) === false &&
is_array($fieldProps['fields']) === true
) {
$index = array_search($fieldName, array_keys($fields));
$before = array_slice($fields, 0, $index);
$after = array_slice($fields, $index + 1);
$fields = array_merge($before, $fieldProps['fields'] ?? [], $after);
$fields = [
...array_slice($fields, 0, $index),
...$fieldProps['fields'] ?? [],
...array_slice($fields, $index + 1)
];
} else {
unset($fields[$fieldName]);
}
@@ -515,11 +635,9 @@ class Blueprint
/**
* Normalizes blueprint options. This must be used in the
* constructor of an extended class, if you want to make use of it.
*
* @param array|true|false|null|string $options
*/
protected function normalizeOptions(
$options,
array|string|bool|null $options,
array $defaults,
array $aliases = []
): array {
@@ -545,7 +663,7 @@ class Blueprint
}
}
return array_merge($defaults, $options);
return [...$defaults, ...$options];
}
/**
@@ -570,10 +688,11 @@ class Blueprint
// inject all section extensions
$sectionProps = $this->extend($sectionProps);
$sections[$sectionName] = $sectionProps = array_merge($sectionProps, [
$sections[$sectionName] = $sectionProps = [
...$sectionProps,
'name' => $sectionName,
'type' => $type = $sectionProps['type'] ?? $sectionName
]);
];
if (empty($type) === true || is_string($type) === false) {
$sections[$sectionName] = [
@@ -623,7 +742,7 @@ class Blueprint
}
// store all normalized sections
$this->sections = array_merge($this->sections, $sections);
$this->sections = [...$this->sections, ...$sections];
return $sections;
}
@@ -653,13 +772,14 @@ class Blueprint
$tabProps = $this->convertFieldsToSections($tabName, $tabProps);
$tabProps = $this->convertSectionsToColumns($tabName, $tabProps);
$tabs[$tabName] = array_merge($tabProps, [
$tabs[$tabName] = [
...$tabProps,
'columns' => $this->normalizeColumns($tabName, $tabProps['columns'] ?? []),
'icon' => $tabProps['icon'] ?? null,
'label' => $this->i18n($tabProps['label'] ?? ucfirst($tabName)),
'link' => $this->model->panel()->url(true) . '/?tab=' . $tabName,
'name' => $tabName,
]);
];
}
return $this->tabs = $tabs;

View File

@@ -157,72 +157,17 @@ class File extends ModelWithContent
*/
public function blueprints(string $inSection = null): array
{
// get cached results for the current file model
// (except when collecting for a specific section)
if ($inSection === null && $this->blueprints !== null) {
return $this->blueprints; // @codeCoverageIgnore
}
// always include the current template as option
$template = $this->template() ?? 'default';
$templates = [$template];
$parent = $this->parent();
// what file templates/blueprints should be considered is
// defined bythe parent's blueprint: which templates it allows
// in files sections as well as files fields
$blueprint = $parent->blueprint();
$fromFields = function ($fields) use (&$fromFields, $parent) {
$templates = [];
foreach ($fields as $field) {
// files or textare field
if (
$field['type'] === 'files' ||
$field['type'] === 'textarea'
) {
$uploads = $field['uploads'] ?? null;
// only if the `uploads` parent is the actual parent
if ($target = $uploads['parent'] ?? null) {
if ($parent->id() !== $target) {
continue;
}
}
$templates[] = $uploads['template'] ?? 'default';
continue;
}
// structure field
if ($field['type'] === 'structure') {
$fields = $fromFields($field['fields']);
$templates = array_merge($templates, $fields);
continue;
}
}
return $templates;
};
// collect all allowed templates…
foreach ($blueprint->sections() as $section) {
// if collecting for a specific section, skip all others
if ($inSection !== null && $section->name() !== $inSection) {
continue;
}
// …from files sections
if ($section->type() === 'files') {
$templates[] = $section->template() ?? 'default';
continue;
}
// …from fields
if ($section->type() === 'fields') {
$fields = $fromFields($section->fields());
$templates = array_merge($templates, $fields);
}
}
$templates = [
$this->template() ?? 'default',
...$this->parent()->blueprint()->acceptedFileTemplates($inSection)
];
// make sure every template is only included once
$templates = array_unique(array_filter($templates));

View File

@@ -100,6 +100,11 @@ trait FileActions
*/
public function changeSort(int $sort): static
{
// skip if the sort number stays the same
if ($this->sort()->value() === $sort) {
return $this;
}
return $this->commit(
'changeSort',
['file' => $this, 'position' => $sort],

View File

@@ -57,6 +57,9 @@ class FileBlueprint extends Blueprint
/**
* Returns the list of all accepted MIME types for
* file upload or `*` if all MIME types are allowed
*
* @deprecated 4.2.0 Use `acceptAttribute` instead
* @todo 5.0.0 Remove method
*/
public function acceptMime(): string
{
@@ -116,6 +119,74 @@ class FileBlueprint extends Blueprint
return '*';
}
/**
* Returns the list of all accepted file extensions
* for file upload or `*` if all extensions are allowed
*
* If a MIME type is specified in the blueprint, the `extension` and `type` options are ignored for the browser.
* Extensions and types, however, are still used to validate an uploaded file on the server.
* This behavior might change in the future to better represent which file extensions are actually allowed.
*
* If no MIME type is specified, the intersection between manually defined extensions and the Kirby "file types" is returned.
* If the intersection is empty, an empty string is returned.
* This behavior might change in the future to instead return the union of `mime`, `extension` and `type`.
*
* @since 4.2.0
*/
public function acceptAttribute(): string
{
// don't disclose the specific default types
if ($this->defaultTypes === true) {
return '*';
}
$accept = $this->accept();
// get extensions from "mime" option
if (is_array($accept['mime']) === true) {
// determine the extensions for each MIME type
$extensions = array_map(
fn ($pattern) => Mime::toExtensions($pattern, true),
$accept['mime']
);
$fromMime = array_unique(array_merge(...array_values($extensions)));
// return early to ignore the other options
return implode(',', array_map(fn ($ext) => ".$ext", $fromMime));
}
$restrictions = [];
// get extensions from "type" option
if (is_array($accept['type']) === true) {
$extensions = array_map(
fn ($type) => F::typeToExtensions($type) ?? [],
$accept['type']
);
$fromType = array_merge(...array_values($extensions));
$restrictions[] = $fromType;
}
// get extensions from "extension" option
if (is_array($accept['extension']) === true) {
$restrictions[] = $accept['extension'];
}
// intersect all restrictions
$list = match (count($restrictions)) {
0 => [],
1 => $restrictions[0],
default => array_intersect(...$restrictions)
};
$list = array_unique($list);
// format the list to include a leading dot on each extension
return implode(',', array_map(fn ($ext) => ".$ext", $list));
}
protected function normalizeAccept(mixed $accept = null): array
{
$accept = match (true) {

View File

@@ -110,6 +110,9 @@ class Helpers
) {
$override = null;
/**
* @psalm-suppress UndefinedVariable
*/
$handler = set_error_handler(function () use (&$override, &$handler, $condition, $fallback) {
// check if suppress condition is met
$suppress = $condition(...func_get_args());

View File

@@ -2,6 +2,7 @@
namespace Kirby\Cms;
use Closure;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Toolkit\Component;
@@ -46,6 +47,21 @@ class Section extends Component
parent::__construct($type, $attrs);
}
/**
* Returns field api call
*/
public function api(): mixed
{
if (
isset($this->options['api']) === true &&
$this->options['api'] instanceof Closure
) {
return $this->options['api']->call($this);
}
return null;
}
public function errors(): array
{
if (array_key_exists('errors', $this->methods) === true) {

View File

@@ -158,7 +158,9 @@ class UpdateStatus
// collect all matching custom messages
$filters = [
'kirby' => $this->app->version(),
'php' => phpversion()
// some PHP version strings contain extra info that makes them
// invalid so we need to strip it off
'php' => preg_replace('/^([^~+-]+).*$/', '$1', phpversion())
];
if ($type === 'plugin') {