Upgrade to Kirby 3.3.2

This commit is contained in:
Bastian Allgeier
2019-12-17 10:31:21 +01:00
parent 3a82406bed
commit 91919964db
20 changed files with 361 additions and 181 deletions

View File

@@ -69,6 +69,13 @@ trait AppPlugins
'validators' => []
];
/**
* Cache for system extensions
*
* @var array
*/
protected static $systemExtensions = null;
/**
* Flag when plugins have been loaded
* to not load them again
@@ -570,37 +577,69 @@ trait AppPlugins
*/
protected function extensionsFromSystem()
{
// Form Field Mixins
FormField::$mixins['filepicker'] = include static::$root . '/config/fields/mixins/filepicker.php';
FormField::$mixins['min'] = include static::$root . '/config/fields/mixins/min.php';
FormField::$mixins['options'] = include static::$root . '/config/fields/mixins/options.php';
FormField::$mixins['pagepicker'] = include static::$root . '/config/fields/mixins/pagepicker.php';
FormField::$mixins['picker'] = include static::$root . '/config/fields/mixins/picker.php';
FormField::$mixins['upload'] = include static::$root . '/config/fields/mixins/upload.php';
FormField::$mixins['userpicker'] = include static::$root . '/config/fields/mixins/userpicker.php';
// load static extensions only once
if (static::$systemExtensions === null) {
// Form Field Mixins
FormField::$mixins['filepicker'] = include static::$root . '/config/fields/mixins/filepicker.php';
FormField::$mixins['min'] = include static::$root . '/config/fields/mixins/min.php';
FormField::$mixins['options'] = include static::$root . '/config/fields/mixins/options.php';
FormField::$mixins['pagepicker'] = include static::$root . '/config/fields/mixins/pagepicker.php';
FormField::$mixins['picker'] = include static::$root . '/config/fields/mixins/picker.php';
FormField::$mixins['upload'] = include static::$root . '/config/fields/mixins/upload.php';
FormField::$mixins['userpicker'] = include static::$root . '/config/fields/mixins/userpicker.php';
// Tag Aliases
KirbyTag::$aliases = [
'youtube' => 'video',
'vimeo' => 'video'
];
// Tag Aliases
KirbyTag::$aliases = [
'youtube' => 'video',
'vimeo' => 'video'
];
// Field method aliases
Field::$aliases = [
'bool' => 'toBool',
'esc' => 'escape',
'excerpt' => 'toExcerpt',
'float' => 'toFloat',
'h' => 'html',
'int' => 'toInt',
'kt' => 'kirbytext',
'kti' => 'kirbytextinline',
'link' => 'toLink',
'md' => 'markdown',
'sp' => 'smartypants',
'v' => 'isValid',
'x' => 'xml'
];
// Field method aliases
Field::$aliases = [
'bool' => 'toBool',
'esc' => 'escape',
'excerpt' => 'toExcerpt',
'float' => 'toFloat',
'h' => 'html',
'int' => 'toInt',
'kt' => 'kirbytext',
'kti' => 'kirbytextinline',
'link' => 'toLink',
'md' => 'markdown',
'sp' => 'smartypants',
'v' => 'isValid',
'x' => 'xml'
];
// blueprint presets
PageBlueprint::$presets['pages'] = include static::$root . '/config/presets/pages.php';
PageBlueprint::$presets['page'] = include static::$root . '/config/presets/page.php';
PageBlueprint::$presets['files'] = include static::$root . '/config/presets/files.php';
// section mixins
Section::$mixins['empty'] = include static::$root . '/config/sections/mixins/empty.php';
Section::$mixins['headline'] = include static::$root . '/config/sections/mixins/headline.php';
Section::$mixins['help'] = include static::$root . '/config/sections/mixins/help.php';
Section::$mixins['layout'] = include static::$root . '/config/sections/mixins/layout.php';
Section::$mixins['max'] = include static::$root . '/config/sections/mixins/max.php';
Section::$mixins['min'] = include static::$root . '/config/sections/mixins/min.php';
Section::$mixins['pagination'] = include static::$root . '/config/sections/mixins/pagination.php';
Section::$mixins['parent'] = include static::$root . '/config/sections/mixins/parent.php';
// section types
Section::$types['info'] = include static::$root . '/config/sections/info.php';
Section::$types['pages'] = include static::$root . '/config/sections/pages.php';
Section::$types['files'] = include static::$root . '/config/sections/files.php';
Section::$types['fields'] = include static::$root . '/config/sections/fields.php';
static::$systemExtensions = [
'components' => include static::$root . '/config/components.php',
'blueprints' => include static::$root . '/config/blueprints.php',
'fields' => include static::$root . '/config/fields.php',
'fieldMethods' => include static::$root . '/config/methods.php',
'tags' => include static::$root . '/config/tags.php'
];
}
// default cache types
$this->extendCacheTypes([
@@ -610,32 +649,11 @@ trait AppPlugins
'memory' => 'Kirby\Cache\MemoryCache',
]);
$this->extendComponents(include static::$root . '/config/components.php');
$this->extendBlueprints(include static::$root . '/config/blueprints.php');
$this->extendFields(include static::$root . '/config/fields.php');
$this->extendFieldMethods((include static::$root . '/config/methods.php')($this));
$this->extendTags(include static::$root . '/config/tags.php');
// blueprint presets
PageBlueprint::$presets['pages'] = include static::$root . '/config/presets/pages.php';
PageBlueprint::$presets['page'] = include static::$root . '/config/presets/page.php';
PageBlueprint::$presets['files'] = include static::$root . '/config/presets/files.php';
// section mixins
Section::$mixins['empty'] = include static::$root . '/config/sections/mixins/empty.php';
Section::$mixins['headline'] = include static::$root . '/config/sections/mixins/headline.php';
Section::$mixins['help'] = include static::$root . '/config/sections/mixins/help.php';
Section::$mixins['layout'] = include static::$root . '/config/sections/mixins/layout.php';
Section::$mixins['max'] = include static::$root . '/config/sections/mixins/max.php';
Section::$mixins['min'] = include static::$root . '/config/sections/mixins/min.php';
Section::$mixins['pagination'] = include static::$root . '/config/sections/mixins/pagination.php';
Section::$mixins['parent'] = include static::$root . '/config/sections/mixins/parent.php';
// section types
Section::$types['info'] = include static::$root . '/config/sections/info.php';
Section::$types['pages'] = include static::$root . '/config/sections/pages.php';
Section::$types['files'] = include static::$root . '/config/sections/files.php';
Section::$types['fields'] = include static::$root . '/config/sections/fields.php';
$this->extendComponents(static::$systemExtensions['components']);
$this->extendBlueprints(static::$systemExtensions['blueprints']);
$this->extendFields(static::$systemExtensions['fields']);
$this->extendFieldMethods((static::$systemExtensions['fieldMethods'])($this));
$this->extendTags(static::$systemExtensions['tags']);
}
/**

View File

@@ -319,6 +319,9 @@ class Auth
$log['by-ip'] = $log['by-ip'] ?? [];
$log['by-email'] = $log['by-email'] ?? [];
// remove all elements on the top level with different keys (old structure)
$log = array_intersect_key($log, array_flip(['by-ip', 'by-email']));
// remove entries that are no longer needed
$originalLog = $log;
$time = time() - $this->kirby->option('auth.timeout', 3600);
@@ -328,9 +331,6 @@ class Auth
});
}
// remove all elements on the top level with different keys (old structure)
$log = array_intersect_key($log, array_flip(['by-ip', 'by-email']));
// write new log to the file system if it changed
if ($read === false || $log !== $originalLog) {
if (count($log['by-ip']) === 0 && count($log['by-email']) === 0) {

View File

@@ -2,10 +2,11 @@
namespace Kirby\Cms;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Exception\NotFoundException;
/**
* Wrapper around our PHPMailer package, which
* Wrapper around our Email package, which
* handles all the magic connections between Kirby
* and sending emails, like email templates, file
* attachments, etc.
@@ -18,26 +19,34 @@ use Kirby\Exception\NotFoundException;
*/
class Email
{
/**
* Options configured through the `email` CMS option
*
* @var array
*/
protected $options;
protected $preset;
/**
* Props for the email object; will be passed to the
* Kirby\Email\Email class
*
* @var array
*/
protected $props;
protected static $transform = [
'from' => 'user',
'replyTo' => 'user',
'to' => 'user',
'cc' => 'user',
'bcc' => 'user',
'attachments' => 'file'
];
/**
* Class constructor
*
* @param string|array $preset Preset name from the config or a simple props array
* @param array $props Props array to override the $preset
*/
public function __construct($preset = [], array $props = [])
{
$this->options = App::instance()->option('email');
// load presets from options
$this->preset = $this->preset($preset);
$this->props = array_merge($this->preset, $props);
// build a prop array based on preset and props
$preset = $this->preset($preset);
$this->props = array_merge($preset, $props);
// add transport settings
if (isset($this->props['transport']) === false) {
@@ -45,27 +54,33 @@ class Email
}
// transform model objects to values
foreach (static::$transform as $prop => $model) {
$this->transformProp($prop, $model);
}
$this->transformUserSingle('from', 'fromName');
$this->transformUserSingle('replyTo', 'replyToName');
$this->transformUserMultiple('to');
$this->transformUserMultiple('cc');
$this->transformUserMultiple('bcc');
$this->transformFile('attachments');
// load template for body text
$this->template();
}
/**
* @param string|array $preset
* Grabs a preset from the options; supports fixed
* prop arrays in case a preset is not needed
*
* @param string|array $preset Preset name or simple prop array
* @return array
*/
protected function preset($preset): array
{
// only passed props, not preset name
if (is_string($preset) !== true) {
if (is_array($preset) === true) {
return $preset;
}
// preset does not exist
if (isset($this->options['presets'][$preset]) === false) {
if (isset($this->options['presets'][$preset]) !== true) {
throw new NotFoundException([
'key' => 'email.preset.notFound',
'data' => ['name' => $preset]
@@ -75,6 +90,12 @@ class Email
return $this->options['presets'][$preset];
}
/**
* Renders the email template(s) and sets the body props
* to the result
*
* @return void
*/
protected function template(): void
{
if (isset($this->props['template']) === true) {
@@ -105,10 +126,10 @@ class Email
}
/**
* Undocumented function
* Returns an email template by name and type
*
* @param string $name
* @param string|null $type
* @param string $name Template name
* @param string|null $type `html` or `text`
* @return \Kirby\Cms\Template
*/
protected function getTemplate(string $name, string $type = null)
@@ -116,47 +137,112 @@ class Email
return App::instance()->template('emails/' . $name, $type, 'text');
}
/**
* Returns the prop array
*
* @return array
*/
public function toArray(): array
{
return $this->props;
}
protected function transformFile($file)
/**
* Transforms file object(s) to an array of file roots;
* supports simple strings, file objects or collections/arrays of either
*
* @param string $prop Prop to transform
* @return void
*/
protected function transformFile(string $prop): void
{
return $this->transformModel($file, 'Kirby\Cms\File', 'root');
$this->props[$prop] = $this->transformModel($prop, 'Kirby\Cms\File', 'root');
}
protected function transformModel($value, $class, $content)
/**
* Transforms Kirby models to a simplified collection
*
* @param string $prop Prop to transform
* @param string $class Fully qualified class name of the supported model
* @param string $contentValue Model method that returns the array value
* @param string|null $contentKey Optional model method that returns the array key;
* returns a simple value-only array if not given
* @return array Simple key-value or just value array with the transformed prop data
*/
protected function transformModel(string $prop, string $class, string $contentValue, string $contentKey = null): array
{
// value is already a string
if (is_string($value) === true) {
return $value;
$value = $this->props[$prop] ?? [];
// ensure consistent input by making everything an iterable value
if (is_iterable($value) !== true) {
$value = [$value];
}
// value is a model object, get value through content method
if (is_a($value, $class) === true) {
return $value->$content();
}
// value is an array or collection, call transform on each item
if (is_array($value) === true || is_a($value, 'Kirby\Cms\Collection') === true) {
$models = [];
foreach ($value as $model) {
$models[] = $this->transformModel($model, $class, $content);
$result = [];
foreach ($value as $key => $item) {
if (is_string($item) === true) {
// value is already a string
if ($contentKey !== null && is_string($key) === true) {
$result[$key] = $item;
} else {
$result[] = $item;
}
} elseif (is_a($item, $class) === true) {
// value is a model object, get value through content method(s)
if ($contentKey !== null) {
$result[(string)$item->$contentKey()] = (string)$item->$contentValue();
} else {
$result[] = (string)$item->$contentValue();
}
} else {
// invalid input
throw new InvalidArgumentException('Invalid input for prop "' . $prop . '", expected string or "' . $class . '" object or collection');
}
return $models;
}
return $result;
}
/**
* Transforms an user object to the email address and name;
* supports simple strings, user objects or collections/arrays of either
* (note: only the first item in a collection/array will be used)
*
* @param string $addressProp Prop with the email address
* @param string $nameProp Prop with the name corresponding to the $addressProp
* @return void
*/
protected function transformUserSingle(string $addressProp, string $nameProp): void
{
$result = $this->transformModel($addressProp, 'Kirby\Cms\User', 'name', 'email');
$address = array_keys($result)[0] ?? null;
$name = $result[$address] ?? null;
// if the array is non-associative, the value is the address
if (is_int($address) === true) {
$address = $name;
$name = null;
}
// always use the address as we have transformed that prop above
$this->props[$addressProp] = $address;
// only use the name from the user if no custom name was set
if (isset($this->props[$nameProp]) === false || $this->props[$nameProp] === null) {
$this->props[$nameProp] = $name;
}
}
protected function transformProp(string $prop, string $model): void
/**
* Transforms user object(s) to the email address(es) and name(s);
* supports simple strings, user objects or collections/arrays of either
*
* @param string $prop Prop to transform
* @return void
*/
protected function transformUserMultiple(string $prop): void
{
if (isset($this->props[$prop]) === true) {
$this->props[$prop] = $this->{'transform' . ucfirst($model)}($this->props[$prop]);
}
}
protected function transformUser($user)
{
return $this->transformModel($user, 'Kirby\Cms\User', 'email');
$this->props[$prop] = $this->transformModel($prop, 'Kirby\Cms\User', 'name', 'email');
}
}

View File

@@ -15,7 +15,9 @@ use Kirby\Toolkit\Properties;
*/
class FileVersion
{
use FileFoundation;
use FileFoundation {
toArray as parentToArray;
}
use Properties;
protected $modifications;
@@ -89,7 +91,7 @@ class FileVersion
*/
public function toArray(): array
{
$array = array_merge(parent::toArray(), [
$array = array_merge($this->parentToArray(), [
'modifications' => $this->modifications(),
]);

View File

@@ -51,7 +51,13 @@ trait PageActions
// actually move the page on disk
if ($oldPage->exists() === true) {
Dir::move($oldPage->root(), $newPage->root());
if (Dir::move($oldPage->root(), $newPage->root()) === true) {
// Updates the root path of the old page with the root path
// of the moved new page to use fly actions on old page in loop
$oldPage->setRoot($newPage->root());
} else {
throw new LogicException('The page directory cannot be moved');
}
}
// overwrite the child in the parent page
@@ -506,10 +512,14 @@ trait PageActions
return $num;
default:
// get instance with default language
$app = $this->kirby()->clone();
$app->setCurrentLanguage();
$template = Str::template($mode, [
'kirby' => $this->kirby(),
'page' => $this,
'site' => $this->site(),
'kirby' => $app,
'page' => $app->page($this->id()),
'site' => $app->site(),
]);
return (int)$template;
@@ -706,7 +716,7 @@ trait PageActions
$sibling->changeNum($index);
}
$parent->children = $siblings->sortBy('num', 'desc');
$parent->children = $siblings->sortBy('num', 'asc');
}
return true;

View File

@@ -272,7 +272,8 @@ class User extends ModelWithContent
}
/**
* Hashes user password
* Hashes the user's password unless it is `null`,
* which will leave it as `null`
*
* @internal
* @param string|null $password
@@ -281,11 +282,7 @@ class User extends ModelWithContent
public static function hashPassword($password): ?string
{
if ($password !== null) {
$info = password_get_info($password);
if ($info['algo'] === 0) {
$password = password_hash($password, PASSWORD_DEFAULT);
}
$password = password_hash($password, PASSWORD_DEFAULT);
}
return $password;
@@ -773,7 +770,7 @@ class User extends ModelWithContent
}
/**
* Sets and hashes a new user password
* Sets the user's password hash
*
* @param string $password
* @return self

View File

@@ -94,7 +94,7 @@ trait UserActions
{
return $this->commit('changePassword', [$this, $password], function ($user, $password) {
$user = $user->clone([
'password' => $password = $user->hashPassword($password)
'password' => $password = User::hashPassword($password)
]);
$user->writePassword($password);
@@ -165,7 +165,7 @@ trait UserActions
$data = $props;
if (isset($props['password']) === true) {
$data['password'] = static::hashPassword($props['password']);
$data['password'] = User::hashPassword($props['password']);
}
$props['role'] = $props['model'] = strtolower($props['role'] ?? 'default');

View File

@@ -25,7 +25,9 @@ class Email
protected $bcc;
protected $cc;
protected $from;
protected $fromName;
protected $replyTo;
protected $replyToName;
protected $isSent = false;
protected $subject;
protected $to;
@@ -75,6 +77,11 @@ class Email
return $this->from;
}
public function fromName(): ?string
{
return $this->fromName;
}
public function isHtml()
{
return $this->body()->html() !== null;
@@ -90,6 +97,11 @@ class Email
return $this->replyTo;
}
public function replyToName(): ?string
{
return $this->replyToName;
}
protected function resolveEmail($email = null, bool $multiple = true)
{
if ($email === null) {
@@ -97,16 +109,27 @@ class Email
}
if (is_array($email) === false) {
$email = [$email];
$email = [$email => null];
}
foreach ($email as $address) {
$result = [];
foreach ($email as $address => $name) {
// convert simple email arrays to associative arrays
if (is_int($address) === true) {
// the value is the address, there is no name
$address = $name;
$result[$address] = null;
} else {
$result[$address] = $name;
}
// ensure that the address is valid
if (V::email($address) === false) {
throw new Exception(sprintf('"%s" is not a valid email address', $address));
}
}
return $multiple === true ? $email : $email[0];
return $multiple === true ? $result : array_keys($result)[0];
}
public function send(): bool
@@ -148,12 +171,24 @@ class Email
return $this;
}
protected function setFromName(string $fromName = null)
{
$this->fromName = $fromName;
return $this;
}
protected function setReplyTo(string $replyTo = null)
{
$this->replyTo = $this->resolveEmail($replyTo, false);
return $this;
}
protected function setReplyToName(string $replyToName = null)
{
$this->replyToName = $replyToName;
return $this;
}
protected function setSubject(string $subject)
{
$this->subject = $subject;

View File

@@ -21,22 +21,22 @@ class PHPMailer extends Email
$mailer = new Mailer(true);
// set sender's address
$mailer->setFrom($this->from());
$mailer->setFrom($this->from(), $this->fromName() ?? '');
// optional reply-to address
if ($replyTo = $this->replyTo()) {
$mailer->addReplyTo($replyTo);
$mailer->addReplyTo($replyTo, $this->replyToName() ?? '');
}
// add (multiple) recepient, CC & BCC addresses
foreach ($this->to() as $to) {
$mailer->addAddress($to);
foreach ($this->to() as $email => $name) {
$mailer->addAddress($email, $name ?? '');
}
foreach ($this->cc() as $cc) {
$mailer->addCC($cc);
foreach ($this->cc() as $email => $name) {
$mailer->addCC($email, $name ?? '');
}
foreach ($this->bcc() as $bcc) {
$mailer->addBCC($bcc);
foreach ($this->bcc() as $email => $name) {
$mailer->addBCC($email, $name ?? '');
}
$mailer->Subject = $this->subject();

View File

@@ -22,16 +22,17 @@ class Remote
* @var array
*/
public static $defaults = [
'agent' => null,
'body' => true,
'data' => [],
'encoding' => 'utf-8',
'file' => null,
'headers' => [],
'method' => 'GET',
'progress' => null,
'test' => false,
'timeout' => 10,
'agent' => null,
'basicAuth' => null,
'body' => true,
'data' => [],
'encoding' => 'utf-8',
'file' => null,
'headers' => [],
'method' => 'GET',
'progress' => null,
'test' => false,
'timeout' => 10,
];
/**
@@ -170,7 +171,22 @@ class Remote
// add all headers
if (empty($this->options['headers']) === false) {
$this->curlopt[CURLOPT_HTTPHEADER] = $this->options['headers'];
// convert associative arrays to strings
$headers = [];
foreach ($this->options['headers'] as $key => $value) {
if (is_string($key) === true) {
$headers[] = $key . ': ' . $value;
} else {
$headers[] = $value;
}
}
$this->curlopt[CURLOPT_HTTPHEADER] = $headers;
}
// add HTTP Basic authentication
if (empty($this->options['basicAuth']) === false) {
$this->curlopt[CURLOPT_USERPWD] = $this->options['basicAuth'];
}
// add the user agent

View File

@@ -72,11 +72,16 @@ class File
/**
* Returns the file as data uri
*
* @param bool $base64 Whether the data should be base64 encoded or not
* @return string
*/
public function dataUri(): string
public function dataUri(bool $base64 = true): string
{
return 'data:' . $this->mime() . ';base64,' . $this->base64();
if ($base64 === true) {
return 'data:' . $this->mime() . ';base64,' . $this->base64();
}
return 'data:' . $this->mime() . ',' . Escape::url($this->read());
}
/**

View File

@@ -31,7 +31,7 @@ class Mime
'avi' => 'video/x-msvideo',
'bmp' => 'image/bmp',
'css' => 'text/css',
'csv' => ['text/x-comma-separated-values', 'text/comma-separated-values', 'application/octet-stream'],
'csv' => ['text/csv', 'text/x-comma-separated-values', 'text/comma-separated-values', 'application/octet-stream'],
'doc' => 'application/msword',
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',