Upgrade to 4.2.0
This commit is contained in:
@@ -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
|
||||
*
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -305,7 +305,23 @@ class Database
|
||||
// try to prepare and execute the sql
|
||||
try {
|
||||
$this->statement = $this->connection->prepare($query);
|
||||
$this->statement->execute($bindings);
|
||||
// bind parameters to statement
|
||||
foreach ($bindings as $parameter => $value) {
|
||||
// positional parameters start at 1
|
||||
if (is_int($parameter)) {
|
||||
$parameter++;
|
||||
}
|
||||
|
||||
$type = match (gettype($value)) {
|
||||
'integer' => PDO::PARAM_INT,
|
||||
'boolean' => PDO::PARAM_BOOL,
|
||||
'NULL' => PDO::PARAM_NULL,
|
||||
default => PDO::PARAM_STR
|
||||
};
|
||||
|
||||
$this->statement->bindValue($parameter, $value, $type);
|
||||
}
|
||||
$this->statement->execute();
|
||||
|
||||
$this->affected = $this->statement->rowCount();
|
||||
$this->lastId = Str::startsWith($query, 'insert ', true) ? $this->connection->lastInsertId() : null;
|
||||
|
||||
@@ -869,7 +869,7 @@ class Query
|
||||
$this->bindings($args[1]);
|
||||
|
||||
// ->where('username like ?', 'myuser')
|
||||
} elseif (is_string($args[0]) === true && is_string($args[1]) === true) {
|
||||
} elseif (is_string($args[0]) === true && is_scalar($args[1]) === true) {
|
||||
// prepared where clause
|
||||
$result = $args[0];
|
||||
|
||||
@@ -887,9 +887,10 @@ class Query
|
||||
$key = $sql->columnName($this->table, $args[0]);
|
||||
|
||||
// ->where('username', 'in', ['myuser', 'myotheruser']);
|
||||
// ->where('quantity', 'between', [10, 50]);
|
||||
$predicate = trim(strtoupper($args[1]));
|
||||
if (is_array($args[2]) === true) {
|
||||
if (in_array($predicate, ['IN', 'NOT IN']) === false) {
|
||||
if (in_array($predicate, ['IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN']) === false) {
|
||||
throw new InvalidArgumentException('Invalid predicate ' . $predicate);
|
||||
}
|
||||
|
||||
@@ -903,15 +904,20 @@ class Query
|
||||
$values[] = $valueBinding;
|
||||
}
|
||||
|
||||
// add that to the where clause in parenthesis
|
||||
$result = $key . ' ' . $predicate . ' (' . implode(', ', $values) . ')';
|
||||
// add that to the where clause in parenthesis or seperated by AND
|
||||
$values = match ($predicate) {
|
||||
'IN',
|
||||
'NOT IN' => '(' . implode(', ', $values) . ')',
|
||||
'BETWEEN',
|
||||
'NOT BETWEEN' => $values[0] . ' AND ' . $values[1]
|
||||
};
|
||||
$result = $key . ' ' . $predicate . ' ' . $values;
|
||||
|
||||
// ->where('username', 'like', 'myuser');
|
||||
} else {
|
||||
$predicates = [
|
||||
'=', '>=', '>', '<=', '<', '<>', '!=', '<=>',
|
||||
'IS', 'IS NOT',
|
||||
'BETWEEN', 'NOT BETWEEN',
|
||||
'LIKE', 'NOT LIKE',
|
||||
'SOUNDS LIKE',
|
||||
'REGEXP', 'NOT REGEXP'
|
||||
|
||||
@@ -132,11 +132,13 @@ abstract class Sql
|
||||
{
|
||||
return [
|
||||
'id' => '{{ name }} INT(11) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY',
|
||||
'varchar' => '{{ name }} varchar(255) {{ null }} {{ default }} {{ unique }}',
|
||||
'varchar' => '{{ name }} varchar({{ size }}) {{ null }} {{ default }} {{ unique }}',
|
||||
'text' => '{{ name }} TEXT {{ unique }}',
|
||||
'int' => '{{ name }} INT(11) UNSIGNED {{ null }} {{ default }} {{ unique }}',
|
||||
'int' => '{{ name }} INT(11) {{ unsigned }} {{ null }} {{ default }} {{ unique }}',
|
||||
'timestamp' => '{{ name }} TIMESTAMP {{ null }} {{ default }} {{ unique }}',
|
||||
'bool' => '{{ name }} TINYINT(1) {{ null }} {{ default }} {{ unique }}'
|
||||
'bool' => '{{ name }} TINYINT(1) {{ null }} {{ default }} {{ unique }}',
|
||||
'float' => '{{ name }} DOUBLE {{ null }} {{ default }} {{ unique }}',
|
||||
'decimal' => '{{ name }} DECIMAL({{ precision }}, {{ decimalPlaces }}) {{ null }} {{ default }} {{ unique }}'
|
||||
];
|
||||
}
|
||||
|
||||
@@ -157,6 +159,10 @@ abstract class Sql
|
||||
* @param string $name Column name
|
||||
* @param array $column Column definition array; valid keys:
|
||||
* - `type` (required): Column template to use
|
||||
* - `unsigned`: Whether an int column is signed or unsigned (boolean)
|
||||
* - `size`: The size of varchar (int)
|
||||
* - `precision`: The precision of a decimal type
|
||||
* - `decimalPlaces`: The number of decimal places for a decimal type
|
||||
* - `null`: Whether the column may be NULL (boolean)
|
||||
* - `key`: Index this column is part of; special values `'primary'` for PRIMARY KEY and `true` for automatic naming
|
||||
* - `unique`: Whether the index (or if not set the column itself) has a UNIQUE constraint
|
||||
@@ -191,6 +197,13 @@ abstract class Sql
|
||||
}
|
||||
}
|
||||
|
||||
// unsigned (defaults to true for backwards compatibility)
|
||||
if (isset($column['unsigned']) === true && $column['unsigned'] === false) {
|
||||
$unsigned = '';
|
||||
} else {
|
||||
$unsigned = 'UNSIGNED';
|
||||
}
|
||||
|
||||
// unique
|
||||
$uniqueKey = false;
|
||||
$uniqueColumn = null;
|
||||
@@ -208,11 +221,15 @@ abstract class Sql
|
||||
$columnDefault = $this->columnDefault($name, $column);
|
||||
|
||||
$query = trim(Str::template($template, [
|
||||
'name' => $this->quoteIdentifier($name),
|
||||
'null' => $null,
|
||||
'default' => $columnDefault['query'],
|
||||
'unique' => $uniqueColumn
|
||||
], ['fallback' => '']));
|
||||
'name' => $this->quoteIdentifier($name),
|
||||
'unsigned' => $unsigned,
|
||||
'size' => $column['size'] ?? 255,
|
||||
'precision' => $column['precision'] ?? 14,
|
||||
'decimalPlaces' => $column['decimalPlaces'] ?? 4,
|
||||
'null' => $null,
|
||||
'default' => $columnDefault['query'],
|
||||
'unique' => $uniqueColumn
|
||||
], ['fallback' => '']));
|
||||
|
||||
return [
|
||||
'query' => $query,
|
||||
|
||||
@@ -42,7 +42,9 @@ class Sqlite extends Sql
|
||||
'text' => '{{ name }} TEXT {{ null }} {{ default }} {{ unique }}',
|
||||
'int' => '{{ name }} INTEGER {{ null }} {{ default }} {{ unique }}',
|
||||
'timestamp' => '{{ name }} INTEGER {{ null }} {{ default }} {{ unique }}',
|
||||
'bool' => '{{ name }} INTEGER {{ null }} {{ default }} {{ unique }}'
|
||||
'bool' => '{{ name }} INTEGER {{ null }} {{ default }} {{ unique }}',
|
||||
'float' => '{{ name }} REAL {{ null }} {{ default }} {{ unique }}',
|
||||
'decimal' => '{{ name }} REAL {{ null }} {{ default }} {{ unique }}'
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -220,143 +220,145 @@ class Dir
|
||||
array|null $contentIgnore = null,
|
||||
bool $multilang = false
|
||||
): array {
|
||||
$dir = realpath($dir);
|
||||
|
||||
$inventory = [
|
||||
'children' => [],
|
||||
'files' => [],
|
||||
'template' => 'default',
|
||||
];
|
||||
|
||||
$dir = realpath($dir);
|
||||
|
||||
if ($dir === false) {
|
||||
return $inventory;
|
||||
}
|
||||
|
||||
$items = static::read($dir, $contentIgnore);
|
||||
|
||||
// a temporary store for all content files
|
||||
$content = [];
|
||||
|
||||
// sort all items naturally to avoid sorting issues later
|
||||
// read and sort all items naturally to avoid sorting issues later
|
||||
$items = static::read($dir, $contentIgnore);
|
||||
natsort($items);
|
||||
|
||||
// loop through all directory items and collect all relevant information
|
||||
foreach ($items as $item) {
|
||||
// ignore all items with a leading dot
|
||||
// ignore all items with a leading dot or underscore
|
||||
if (in_array(substr($item, 0, 1), ['.', '_']) === true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$root = $dir . '/' . $item;
|
||||
|
||||
// collect all directories as children
|
||||
if (is_dir($root) === true) {
|
||||
// extract the slug and num of the directory
|
||||
if (preg_match('/^([0-9]+)' . static::$numSeparator . '(.*)$/', $item, $match)) {
|
||||
$num = (int)$match[1];
|
||||
$slug = $match[2];
|
||||
} else {
|
||||
$num = null;
|
||||
$slug = $item;
|
||||
}
|
||||
|
||||
$inventory['children'][] = [
|
||||
'dirname' => $item,
|
||||
'model' => null,
|
||||
'num' => $num,
|
||||
'root' => $root,
|
||||
'slug' => $slug,
|
||||
];
|
||||
} else {
|
||||
$extension = pathinfo($item, PATHINFO_EXTENSION);
|
||||
|
||||
switch ($extension) {
|
||||
case 'htm':
|
||||
case 'html':
|
||||
case 'php':
|
||||
// don't track those files
|
||||
break;
|
||||
case $contentExtension:
|
||||
$content[] = pathinfo($item, PATHINFO_FILENAME);
|
||||
break;
|
||||
default:
|
||||
$inventory['files'][$item] = [
|
||||
'filename' => $item,
|
||||
'extension' => $extension,
|
||||
'root' => $root,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove the language codes from all content filenames
|
||||
if ($multilang === true) {
|
||||
foreach ($content as $key => $filename) {
|
||||
$content[$key] = pathinfo($filename, PATHINFO_FILENAME);
|
||||
$inventory['children'][] = static::inventoryChild(
|
||||
$item,
|
||||
$root,
|
||||
$contentExtension,
|
||||
$multilang
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$content = array_unique($content);
|
||||
$extension = pathinfo($item, PATHINFO_EXTENSION);
|
||||
|
||||
// don't track files with these extensions
|
||||
if (in_array($extension, ['htm', 'html', 'php']) === true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// collect all content files separately,
|
||||
// not as inventory entries
|
||||
if ($extension === $contentExtension) {
|
||||
$filename = pathinfo($item, PATHINFO_FILENAME);
|
||||
|
||||
// remove the language codes from all content filenames
|
||||
if ($multilang === true) {
|
||||
$filename = pathinfo($filename, PATHINFO_FILENAME);
|
||||
}
|
||||
|
||||
$content[] = $filename;
|
||||
continue;
|
||||
}
|
||||
|
||||
// collect all other files
|
||||
$inventory['files'][$item] = [
|
||||
'filename' => $item,
|
||||
'extension' => $extension,
|
||||
'root' => $root,
|
||||
];
|
||||
}
|
||||
|
||||
$inventory = static::inventoryContent($inventory, $content);
|
||||
$inventory = static::inventoryModels($inventory, $contentExtension, $multilang);
|
||||
$content = array_unique($content);
|
||||
|
||||
$inventory['template'] = static::inventoryTemplate(
|
||||
$content,
|
||||
$inventory['files']
|
||||
);
|
||||
|
||||
return $inventory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Take all content files,
|
||||
* remove those who are meta files and
|
||||
* detect the main content file
|
||||
* Collect information for a child for the inventory
|
||||
*/
|
||||
protected static function inventoryContent(array $inventory, array $content): array
|
||||
{
|
||||
// filter meta files from the content file
|
||||
if (empty($content) === true) {
|
||||
$inventory['template'] = 'default';
|
||||
return $inventory;
|
||||
protected static function inventoryChild(
|
||||
string $item,
|
||||
string $root,
|
||||
string $contentExtension = 'txt',
|
||||
bool $multilang = false
|
||||
): array {
|
||||
// extract the slug and num of the directory
|
||||
if ($separator = strpos($item, static::$numSeparator)) {
|
||||
$num = (int)substr($item, 0, $separator);
|
||||
$slug = substr($item, $separator + 1);
|
||||
}
|
||||
|
||||
foreach ($content as $contentName) {
|
||||
// could be a meta file. i.e. cover.jpg
|
||||
if (isset($inventory['files'][$contentName]) === true) {
|
||||
// determine the model
|
||||
if (empty(Page::$models) === false) {
|
||||
if ($multilang === true) {
|
||||
$code = App::instance()->defaultLanguage()->code();
|
||||
$contentExtension = $code . '.' . $contentExtension;
|
||||
}
|
||||
|
||||
// look if a content file can be found
|
||||
// for any of the available models
|
||||
foreach (Page::$models as $modelName => $modelClass) {
|
||||
if (is_file($root . '/' . $modelName . '.' . $contentExtension) === true) {
|
||||
$model = $modelName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'dirname' => $item,
|
||||
'model' => $model ?? null,
|
||||
'num' => $num ?? null,
|
||||
'root' => $root,
|
||||
'slug' => $slug ?? $item,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the main template for the inventory
|
||||
* from all collected content files, ignore file meta files
|
||||
*/
|
||||
protected static function inventoryTemplate(
|
||||
array $content,
|
||||
array $files,
|
||||
): string {
|
||||
foreach ($content as $name) {
|
||||
// is a meta file corresponding to an actual file, i.e. cover.jpg
|
||||
if (isset($files[$name]) === true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// it's most likely the template
|
||||
$inventory['template'] = $contentName;
|
||||
// (will overwrite and use the last match for historic reasons)
|
||||
$template = $name;
|
||||
}
|
||||
|
||||
return $inventory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Go through all inventory children
|
||||
* and inject a model for each
|
||||
*/
|
||||
protected static function inventoryModels(
|
||||
array $inventory,
|
||||
string $contentExtension,
|
||||
bool $multilang = false
|
||||
): array {
|
||||
// inject models
|
||||
if (
|
||||
empty($inventory['children']) === false &&
|
||||
empty(Page::$models) === false
|
||||
) {
|
||||
if ($multilang === true) {
|
||||
$contentExtension = App::instance()->defaultLanguage()->code() . '.' . $contentExtension;
|
||||
}
|
||||
|
||||
foreach ($inventory['children'] as $key => $child) {
|
||||
foreach (Page::$models as $modelName => $modelClass) {
|
||||
if (file_exists($child['root'] . '/' . $modelName . '.' . $contentExtension) === true) {
|
||||
$inventory['children'][$key]['model'] = $modelName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $inventory;
|
||||
return $template ?? 'default';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace Kirby\Filesystem;
|
||||
|
||||
use Kirby\Toolkit\A;
|
||||
use Kirby\Toolkit\Str;
|
||||
use SimpleXMLElement;
|
||||
|
||||
@@ -268,18 +269,32 @@ class Mime
|
||||
/**
|
||||
* Returns all available extensions for a given MIME type
|
||||
*/
|
||||
public static function toExtensions(string $mime = null): array
|
||||
public static function toExtensions(string $mime = null, bool $matchWildcards = false): array
|
||||
{
|
||||
$extensions = [];
|
||||
$testMime = fn (string $v) => static::matches($v, $mime);
|
||||
|
||||
foreach (static::$types as $key => $value) {
|
||||
if (is_array($value) === true && in_array($mime, $value) === true) {
|
||||
$extensions[] = $key;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($value === $mime) {
|
||||
$extensions[] = $key;
|
||||
if (is_array($value) === true) {
|
||||
if ($matchWildcards === true) {
|
||||
if (A::some($value, $testMime)) {
|
||||
$extensions[] = $key;
|
||||
}
|
||||
} else {
|
||||
if (in_array($mime, $value) === true) {
|
||||
$extensions[] = $key;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ($matchWildcards === true) {
|
||||
if ($testMime($value) === true) {
|
||||
$extensions[] = $key;
|
||||
}
|
||||
} else {
|
||||
if ($value === $mime) {
|
||||
$extensions[] = $key;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -62,8 +62,8 @@ class ImageMagick extends Darkroom
|
||||
{
|
||||
$command = escapeshellarg($options['bin']);
|
||||
|
||||
// limit to single-threading to keep CPU usage sane
|
||||
$command .= ' -limit thread 1';
|
||||
// default is limiting to single-threading to keep CPU usage sane
|
||||
$command .= ' -limit thread ' . escapeshellarg($options['threads']);
|
||||
|
||||
// append input file
|
||||
return $command . ' ' . escapeshellarg($file);
|
||||
@@ -77,6 +77,7 @@ class ImageMagick extends Darkroom
|
||||
return parent::defaults() + [
|
||||
'bin' => 'convert',
|
||||
'interlace' => false,
|
||||
'threads' => 1,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -121,35 +121,72 @@ class Assets
|
||||
* Returns array of favicon icons
|
||||
* based on config option
|
||||
*
|
||||
* @todo Deprecate `url` option in v5, use `href` option instead
|
||||
* @todo Deprecate `rel` usage as array key in v5, use `rel` option instead
|
||||
*
|
||||
* @throws \Kirby\Exception\InvalidArgumentException
|
||||
*/
|
||||
public function favicons(): array
|
||||
{
|
||||
$icons = $this->kirby->option('panel.favicon', [
|
||||
'apple-touch-icon' => [
|
||||
'type' => 'image/png',
|
||||
'url' => $this->url . '/apple-touch-icon.png',
|
||||
[
|
||||
'rel' => 'apple-touch-icon',
|
||||
'type' => 'image/png',
|
||||
'href' => $this->url . '/apple-touch-icon.png'
|
||||
],
|
||||
'alternate icon' => [
|
||||
'type' => 'image/png',
|
||||
'url' => $this->url . '/favicon.png',
|
||||
[
|
||||
'rel' => 'alternate icon',
|
||||
'type' => 'image/png',
|
||||
'href' => $this->url . '/favicon.png'
|
||||
],
|
||||
'shortcut icon' => [
|
||||
'type' => 'image/svg+xml',
|
||||
'url' => $this->url . '/favicon.svg',
|
||||
[
|
||||
'rel' => 'shortcut icon',
|
||||
'type' => 'image/svg+xml',
|
||||
'href' => $this->url . '/favicon.svg'
|
||||
],
|
||||
[
|
||||
'rel' => 'apple-touch-icon',
|
||||
'type' => 'image/png',
|
||||
'href' => $this->url . '/apple-touch-icon-dark.png',
|
||||
'media' => '(prefers-color-scheme: dark)'
|
||||
],
|
||||
[
|
||||
'rel' => 'alternate icon',
|
||||
'type' => 'image/png',
|
||||
'href' => $this->url . '/favicon-dark.png',
|
||||
'media' => '(prefers-color-scheme: dark)'
|
||||
]
|
||||
]);
|
||||
|
||||
if (is_array($icons) === true) {
|
||||
return $icons;
|
||||
// normalize options
|
||||
foreach ($icons as $rel => &$icon) {
|
||||
// TODO: remove this backward compatibility check in v6
|
||||
if (isset($icon['url']) === true) {
|
||||
$icon['href'] = $icon['url'];
|
||||
unset($icon['url']);
|
||||
}
|
||||
|
||||
// TODO: remove this backward compatibility check in v6
|
||||
if (is_string($rel) === true && isset($icon['rel']) === false) {
|
||||
$icon['rel'] = $rel;
|
||||
}
|
||||
|
||||
$icon['href'] = Url::to($icon['href']);
|
||||
$icon['nonce'] = $this->nonce;
|
||||
}
|
||||
|
||||
return array_values($icons);
|
||||
}
|
||||
|
||||
// make sure to convert favicon string to array
|
||||
if (is_string($icons) === true) {
|
||||
return [
|
||||
'shortcut icon' => [
|
||||
'type' => F::mime($icons),
|
||||
'url' => $icons,
|
||||
[
|
||||
'rel' => 'shortcut icon',
|
||||
'type' => F::mime($icons),
|
||||
'href' => Url::to($icons),
|
||||
'nonce' => $this->nonce
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
@@ -2,12 +2,10 @@
|
||||
|
||||
namespace Kirby\Panel\Lab;
|
||||
|
||||
use Kirby\Cms\App;
|
||||
use Kirby\Exception\NotFoundException;
|
||||
use Kirby\Filesystem\Dir;
|
||||
use Kirby\Filesystem\F;
|
||||
use Kirby\Http\Response;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* One or multiple lab examples with one or multiple tabs
|
||||
@@ -81,17 +79,6 @@ class Example
|
||||
return $this->parent->root() . '/' . $this->path() . '/' . $filename;
|
||||
}
|
||||
|
||||
public function github(): string
|
||||
{
|
||||
$path = Str::after($this->root(), App::instance()->root('kirby'));
|
||||
|
||||
if ($tab = $this->tab()) {
|
||||
$path .= '/' . $tab;
|
||||
}
|
||||
|
||||
return 'https://github.com/getkirby/kirby/tree/main' . $path;
|
||||
}
|
||||
|
||||
public function id(): string
|
||||
{
|
||||
return $this->id;
|
||||
@@ -204,22 +191,48 @@ class Example
|
||||
$file ??= '';
|
||||
|
||||
// extract parts
|
||||
$parts['template'] = $this->vueTemplate($file);
|
||||
$parts['examples'] = $this->vueExamples($parts['template']);
|
||||
$parts['script'] = $this->vueScript($file);
|
||||
$parts['template'] = $this->vueTemplate($file);
|
||||
$parts['examples'] = $this->vueExamples($parts['template'], $parts['script']);
|
||||
$parts['style'] = $this->vueStyle($file);
|
||||
|
||||
return $parts;
|
||||
}
|
||||
|
||||
public function vueExamples(string|null $template): array
|
||||
public function vueExamples(string|null $template, string|null $script): array
|
||||
{
|
||||
$template ??= '';
|
||||
$examples = [];
|
||||
$scripts = [];
|
||||
|
||||
if (preg_match_all('!<k-lab-example[\s|\n].*?label="(.*?)".*?>(.*?)<\/k-lab-example>!s', $template, $matches)) {
|
||||
if (preg_match_all('!\/\*\* \@script: (.*?)\*\/(.*?)\/\*\* \@script-end \*\/!s', $script, $matches)) {
|
||||
foreach ($matches[1] as $key => $name) {
|
||||
$code = $matches[2][$key];
|
||||
$code = preg_replace('!const (.*?) \=!', 'default', $code);
|
||||
|
||||
$scripts[trim($name)] = $code;
|
||||
}
|
||||
}
|
||||
|
||||
if (preg_match_all('!<k-lab-example[\s|\n].*?label="(.*?)"(.*?)>(.*?)<\/k-lab-example>!s', $template, $matches)) {
|
||||
foreach ($matches[1] as $key => $name) {
|
||||
$tail = $matches[2][$key];
|
||||
$code = $matches[3][$key];
|
||||
|
||||
$scriptId = trim(preg_replace_callback('!script="(.*?)"!', function ($match) {
|
||||
return trim($match[1]);
|
||||
}, $tail));
|
||||
|
||||
$scriptBlock = $scripts[$scriptId] ?? null;
|
||||
|
||||
if (empty($scriptBlock) === false) {
|
||||
$js = PHP_EOL . PHP_EOL;
|
||||
$js .= '<script>';
|
||||
$js .= $scriptBlock;
|
||||
$js .= '</script>';
|
||||
} else {
|
||||
$js = '';
|
||||
}
|
||||
|
||||
// only use the code between the @code and @code-end comments
|
||||
if (preg_match('$<!-- @code -->(.*?)<!-- @code-end -->$s', $code, $match)) {
|
||||
@@ -231,11 +244,21 @@ class Example
|
||||
$indents = array_map(fn ($i) => strlen($i), $indents[1]);
|
||||
$indents = min($indents);
|
||||
|
||||
if (empty($js) === false) {
|
||||
$indents--;
|
||||
}
|
||||
|
||||
// strip minimum indent from each line
|
||||
$code = preg_replace('/^\t{' . $indents . '}/m', '', $code);
|
||||
}
|
||||
|
||||
$examples[$name] = trim($code);
|
||||
$code = trim($code);
|
||||
|
||||
if (empty($js) === false) {
|
||||
$code = '<template>' . PHP_EOL . "\t" . $code . PHP_EOL . '</template>';
|
||||
}
|
||||
|
||||
$examples[$name] = $code . $js;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ class Menu
|
||||
'link' => $area['link'] ?? null,
|
||||
'dialog' => $area['dialog'] ?? null,
|
||||
'drawer' => $area['drawer'] ?? null,
|
||||
'text' => $area['label'],
|
||||
'text' => I18n::translate($area['label'], $area['label'])
|
||||
], $menu);
|
||||
|
||||
// unset the link (which is always added by default to an area)
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
namespace Kirby\Panel;
|
||||
|
||||
use Kirby\Cms\File;
|
||||
use Kirby\Cms\Find;
|
||||
use Kirby\Cms\Page;
|
||||
use Kirby\Cms\PageBlueprint;
|
||||
use Kirby\Cms\PageRules;
|
||||
use Kirby\Cms\Site;
|
||||
use Kirby\Cms\User;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Form\Form;
|
||||
use Kirby\Toolkit\A;
|
||||
@@ -32,7 +34,7 @@ class PageCreateDialog
|
||||
protected string|null $slug;
|
||||
protected string|null $template;
|
||||
protected string|null $title;
|
||||
protected Page|Site $view;
|
||||
protected Page|Site|User|File $view;
|
||||
protected string|null $viewId;
|
||||
|
||||
public static array $fieldTypes = [
|
||||
@@ -369,9 +371,9 @@ class PageCreateDialog
|
||||
$value = [
|
||||
'parent' => $this->parentId,
|
||||
'section' => $this->sectionId,
|
||||
'slug' => '',
|
||||
'slug' => $this->slug ?? '',
|
||||
'template' => $this->template,
|
||||
'title' => '',
|
||||
'title' => $this->title ?? '',
|
||||
'view' => $this->viewId,
|
||||
];
|
||||
|
||||
|
||||
@@ -219,7 +219,7 @@ class KirbyTag
|
||||
*/
|
||||
public function parent(): ModelWithContent|null
|
||||
{
|
||||
return $this->data['parent'];
|
||||
return $this->data['parent'] ?? null;
|
||||
}
|
||||
|
||||
public function render(): string
|
||||
|
||||
@@ -255,7 +255,7 @@ class A
|
||||
* // result: ['cat' => 'miao', 'dog' => 'wuff'];
|
||||
* </code>
|
||||
*
|
||||
* @param array $array The source array
|
||||
* @param mixed $array The source array
|
||||
* @param string|int|array|null $key The key to look for
|
||||
* @param mixed $default Optional default value, which
|
||||
* should be returned if no element
|
||||
@@ -588,7 +588,7 @@ class A
|
||||
*/
|
||||
public static function prepend(array $array, array $prepend): array
|
||||
{
|
||||
return $prepend + $array;
|
||||
return static::merge($prepend, $array, A::MERGE_APPEND);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -107,16 +107,16 @@ class Date extends DateTime
|
||||
static::validateUnit($unit);
|
||||
|
||||
$formats = [
|
||||
'year' => 'Y-01-01P',
|
||||
'month' => 'Y-m-01P',
|
||||
'day' => 'Y-m-dP',
|
||||
'hour' => 'Y-m-d H:00:00P',
|
||||
'minute' => 'Y-m-d H:i:00P',
|
||||
'second' => 'Y-m-d H:i:sP'
|
||||
'year' => 'Y-01-01',
|
||||
'month' => 'Y-m-01',
|
||||
'day' => 'Y-m-d',
|
||||
'hour' => 'Y-m-d H:00:00',
|
||||
'minute' => 'Y-m-d H:i:00',
|
||||
'second' => 'Y-m-d H:i:s'
|
||||
];
|
||||
|
||||
$flooredDate = date($formats[$unit], $this->timestamp());
|
||||
$this->set($flooredDate);
|
||||
$flooredDate = $this->format($formats[$unit]);
|
||||
$this->set($flooredDate, $this->timezone());
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
@@ -710,7 +710,7 @@ class Dom
|
||||
return $options;
|
||||
}
|
||||
|
||||
$options = array_merge([
|
||||
return [
|
||||
'allowedAttrPrefixes' => [],
|
||||
'allowedAttrs' => true,
|
||||
'allowedDataUris' => true,
|
||||
@@ -724,11 +724,9 @@ class Dom
|
||||
'doctypeCallback' => null,
|
||||
'elementCallback' => null,
|
||||
'urlAttrs' => ['href', 'src', 'xlink:href'],
|
||||
], $options);
|
||||
|
||||
$options['_normalized'] = true;
|
||||
|
||||
return $options;
|
||||
...$options,
|
||||
'_normalized' => true
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -283,6 +283,14 @@ V::$validators = [
|
||||
V::max($value, $max) === true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks with the callback sent by the user
|
||||
* It's ideal for one-time custom validations
|
||||
*/
|
||||
'callback' => function ($value, callable $callback): bool {
|
||||
return $callback($value);
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the given string contains the given value
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user