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

@@ -1,7 +1,7 @@
{ {
"name": "getkirby/cms", "name": "getkirby/cms",
"description": "The Kirby 3 core", "description": "The Kirby 3 core",
"version": "3.3.1", "version": "3.3.2",
"license": "proprietary", "license": "proprietary",
"keywords": ["kirby", "cms", "core"], "keywords": ["kirby", "cms", "core"],
"homepage": "https://getkirby.com", "homepage": "https://getkirby.com",

14
kirby/composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "e10295fa58c27caa37474e7fa503bb8e", "content-hash": "be4d625ce32603f849e29b8d2e841006",
"packages": [ "packages": [
{ {
"name": "claviska/simpleimage", "name": "claviska/simpleimage",
@@ -413,16 +413,16 @@
}, },
{ {
"name": "symfony/polyfill-mbstring", "name": "symfony/polyfill-mbstring",
"version": "v1.12.0", "version": "v1.13.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git", "url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "b42a2f66e8f1b15ccf25652c3424265923eb4f17" "reference": "7b4aab9743c30be783b73de055d24a39cf4b954f"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/b42a2f66e8f1b15ccf25652c3424265923eb4f17", "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/7b4aab9743c30be783b73de055d24a39cf4b954f",
"reference": "b42a2f66e8f1b15ccf25652c3424265923eb4f17", "reference": "7b4aab9743c30be783b73de055d24a39cf4b954f",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -434,7 +434,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "1.12-dev" "dev-master": "1.13-dev"
} }
}, },
"autoload": { "autoload": {
@@ -468,7 +468,7 @@
"portable", "portable",
"shim" "shim"
], ],
"time": "2019-08-06T08:03:45+00:00" "time": "2019-11-27T14:18:11+00:00"
}, },
{ {
"name": "true/punycode", "name": "true/punycode",

View File

@@ -438,12 +438,12 @@
"upload.error.default": "El archivo no pudo ser subido", "upload.error.default": "El archivo no pudo ser subido",
"upload.error.extension": "Subida de archivo detenida por la extensión", "upload.error.extension": "Subida de archivo detenida por la extensión",
"upload.error.formSize": "El archivo subido excede la directiva MAX_FILE_SIZE que fue especificada en el formulario", "upload.error.formSize": "El archivo subido excede la directiva MAX_FILE_SIZE que fue especificada en el formulario",
"upload.error.iniPostSize": "The uploaded file exceeds the post_max_size directive in php.ini", "upload.error.iniPostSize": "El archivo subido excede la directiva post_max_size directive en php.ini",
"upload.error.iniSize": "The uploaded file exceeds the upload_max_filesize directive in php.ini", "upload.error.iniSize": "El archivo subido excede la directiva upload_max_filesize en php.ini",
"upload.error.noFile": "No file was uploaded", "upload.error.noFile": "Ningún archivo ha sido subido",
"upload.error.noFiles": "No files were uploaded", "upload.error.noFiles": "Ningún archivo ha sido subido",
"upload.error.partial": "The uploaded file was only partially uploaded", "upload.error.partial": "El archivo ha sido subido solo parcialmente",
"upload.error.tmpDir": "Missing a temporary folder", "upload.error.tmpDir": "No se encuentra la carpeta temporal",
"upload.errors": "Error", "upload.errors": "Error",
"upload.progress": "Subiendo...", "upload.progress": "Subiendo...",

View File

@@ -23,15 +23,15 @@
"delete": "Borrar", "delete": "Borrar",
"dimensions": "Dimensiones", "dimensions": "Dimensiones",
"disabled": "Disabled", "disabled": "Desabilitado",
"discard": "Descartar", "discard": "Descartar",
"download": "Download", "download": "Descargar",
"duplicate": "Duplicate", "duplicate": "Duplicar",
"edit": "Editar", "edit": "Editar",
"dialog.files.empty": "No files to select", "dialog.files.empty": "No se ha seleccionado ningún archivo",
"dialog.pages.empty": "No pages to select", "dialog.pages.empty": "No se ha seleccionado ninguna página",
"dialog.users.empty": "No users to select", "dialog.users.empty": "No se ha seleccionado ningún usuario",
"email": "Correo electrónico", "email": "Correo electrónico",
"email.placeholder": "correo@ejemplo.com", "email.placeholder": "correo@ejemplo.com",

View File

@@ -61,21 +61,21 @@
"Filändelsen \"{extension}\" är inte tillåten", "Filändelsen \"{extension}\" är inte tillåten",
"error.file.extension.missing": "error.file.extension.missing":
"Filen \"{filename}\" saknar filändelse", "Filen \"{filename}\" saknar filändelse",
"error.file.maxheight": "The height of the image must not exceed {height} pixels", "error.file.maxheight": "Bildens höjd får inte överstiga {height} pixlar",
"error.file.maxsize": "The file is too large", "error.file.maxsize": "Filen är för stor",
"error.file.maxwidth": "The width of the image must not exceed {width} pixels", "error.file.maxwidth": "Bildens bredd får inte överstiga {width} pixlar",
"error.file.mime.differs": "error.file.mime.differs":
"Den uppladdade filen måste vara av samma mime-typ \"{mime}\"", "Den uppladdade filen måste vara av samma mime-typ \"{mime}\"",
"error.file.mime.forbidden": "Mediatypen \"{mime}\" är inte tillåten", "error.file.mime.forbidden": "Mediatypen \"{mime}\" är inte tillåten",
"error.file.mime.invalid": "Invalid mime type: {mime}", "error.file.mime.invalid": "Ogiltig mime-typ: {mime}",
"error.file.mime.missing": "error.file.mime.missing":
"Mediatypen för \"{filename}\" kan inte detekteras", "Mediatypen för \"{filename}\" kan inte detekteras",
"error.file.minheight": "The height of the image must be at least {height} pixels", "error.file.minheight": "Bildens höjd måste vara minst {height} pixlar",
"error.file.minsize": "The file is too small", "error.file.minsize": "Filen är för liten",
"error.file.minwidth": "The width of the image must be at least {width} pixels", "error.file.minwidth": "Bildens bredd måste vara minst {width} pixlar",
"error.file.name.missing": "Filnamnet får inte vara tomt", "error.file.name.missing": "Filnamnet får inte vara tomt",
"error.file.notFound": "Filen \"{filename}\" kan ej hittas", "error.file.notFound": "Filen \"{filename}\" kan ej hittas",
"error.file.orientation": "The orientation of the image must be \"{orientation}\"", "error.file.orientation": "Bildens orientering måste vara \"{orientation}\"",
"error.file.type.forbidden": "Du har inte behörighet att ladda upp filer av typen {type}", "error.file.type.forbidden": "Du har inte behörighet att ladda upp filer av typen {type}",
"error.file.undefined": "Filen kan inte hittas", "error.file.undefined": "Filen kan inte hittas",
@@ -304,7 +304,7 @@
"loading": "Laddar", "loading": "Laddar",
"lock.unsaved": "Osparade ändringar", "lock.unsaved": "Osparade ändringar",
"lock.unsaved.empty": "There are no more unsaved changes", "lock.unsaved.empty": "Det finns inga fler osparade ändringar",
"lock.isLocked": "Osparade ändringar av <strong>{email}</strong>", "lock.isLocked": "Osparade ändringar av <strong>{email}</strong>",
"lock.file.isLocked": "Filen redigeras just nu av {email} och kan inte redigeras.", "lock.file.isLocked": "Filen redigeras just nu av {email} och kan inte redigeras.",
"lock.page.isLocked": "Sidan redigeras just nu av {email} och kan inte redigeras.", "lock.page.isLocked": "Sidan redigeras just nu av {email} och kan inte redigeras.",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -69,6 +69,13 @@ trait AppPlugins
'validators' => [] 'validators' => []
]; ];
/**
* Cache for system extensions
*
* @var array
*/
protected static $systemExtensions = null;
/** /**
* Flag when plugins have been loaded * Flag when plugins have been loaded
* to not load them again * to not load them again
@@ -570,6 +577,8 @@ trait AppPlugins
*/ */
protected function extensionsFromSystem() protected function extensionsFromSystem()
{ {
// load static extensions only once
if (static::$systemExtensions === null) {
// Form Field Mixins // Form Field Mixins
FormField::$mixins['filepicker'] = include static::$root . '/config/fields/mixins/filepicker.php'; FormField::$mixins['filepicker'] = include static::$root . '/config/fields/mixins/filepicker.php';
FormField::$mixins['min'] = include static::$root . '/config/fields/mixins/min.php'; FormField::$mixins['min'] = include static::$root . '/config/fields/mixins/min.php';
@@ -602,20 +611,6 @@ trait AppPlugins
'x' => 'xml' 'x' => 'xml'
]; ];
// default cache types
$this->extendCacheTypes([
'apcu' => 'Kirby\Cache\ApcuCache',
'file' => 'Kirby\Cache\FileCache',
'memcached' => 'Kirby\Cache\MemCached',
'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 // blueprint presets
PageBlueprint::$presets['pages'] = include static::$root . '/config/presets/pages.php'; PageBlueprint::$presets['pages'] = include static::$root . '/config/presets/pages.php';
PageBlueprint::$presets['page'] = include static::$root . '/config/presets/page.php'; PageBlueprint::$presets['page'] = include static::$root . '/config/presets/page.php';
@@ -636,6 +631,29 @@ trait AppPlugins
Section::$types['pages'] = include static::$root . '/config/sections/pages.php'; Section::$types['pages'] = include static::$root . '/config/sections/pages.php';
Section::$types['files'] = include static::$root . '/config/sections/files.php'; Section::$types['files'] = include static::$root . '/config/sections/files.php';
Section::$types['fields'] = include static::$root . '/config/sections/fields.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([
'apcu' => 'Kirby\Cache\ApcuCache',
'file' => 'Kirby\Cache\FileCache',
'memcached' => 'Kirby\Cache\MemCached',
'memory' => 'Kirby\Cache\MemoryCache',
]);
$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-ip'] = $log['by-ip'] ?? [];
$log['by-email'] = $log['by-email'] ?? []; $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 // remove entries that are no longer needed
$originalLog = $log; $originalLog = $log;
$time = time() - $this->kirby->option('auth.timeout', 3600); $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 // write new log to the file system if it changed
if ($read === false || $log !== $originalLog) { if ($read === false || $log !== $originalLog) {
if (count($log['by-ip']) === 0 && count($log['by-email']) === 0) { if (count($log['by-ip']) === 0 && count($log['by-email']) === 0) {

View File

@@ -2,10 +2,11 @@
namespace Kirby\Cms; namespace Kirby\Cms;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Exception\NotFoundException; use Kirby\Exception\NotFoundException;
/** /**
* Wrapper around our PHPMailer package, which * Wrapper around our Email package, which
* handles all the magic connections between Kirby * handles all the magic connections between Kirby
* and sending emails, like email templates, file * and sending emails, like email templates, file
* attachments, etc. * attachments, etc.
@@ -18,26 +19,34 @@ use Kirby\Exception\NotFoundException;
*/ */
class Email class Email
{ {
/**
* Options configured through the `email` CMS option
*
* @var array
*/
protected $options; protected $options;
protected $preset;
/**
* Props for the email object; will be passed to the
* Kirby\Email\Email class
*
* @var array
*/
protected $props; protected $props;
protected static $transform = [ /**
'from' => 'user', * Class constructor
'replyTo' => 'user', *
'to' => 'user', * @param string|array $preset Preset name from the config or a simple props array
'cc' => 'user', * @param array $props Props array to override the $preset
'bcc' => 'user', */
'attachments' => 'file'
];
public function __construct($preset = [], array $props = []) public function __construct($preset = [], array $props = [])
{ {
$this->options = App::instance()->option('email'); $this->options = App::instance()->option('email');
// load presets from options // build a prop array based on preset and props
$this->preset = $this->preset($preset); $preset = $this->preset($preset);
$this->props = array_merge($this->preset, $props); $this->props = array_merge($preset, $props);
// add transport settings // add transport settings
if (isset($this->props['transport']) === false) { if (isset($this->props['transport']) === false) {
@@ -45,27 +54,33 @@ class Email
} }
// transform model objects to values // transform model objects to values
foreach (static::$transform as $prop => $model) { $this->transformUserSingle('from', 'fromName');
$this->transformProp($prop, $model); $this->transformUserSingle('replyTo', 'replyToName');
} $this->transformUserMultiple('to');
$this->transformUserMultiple('cc');
$this->transformUserMultiple('bcc');
$this->transformFile('attachments');
// load template for body text // load template for body text
$this->template(); $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 * @return array
*/ */
protected function preset($preset): array protected function preset($preset): array
{ {
// only passed props, not preset name // only passed props, not preset name
if (is_string($preset) !== true) { if (is_array($preset) === true) {
return $preset; return $preset;
} }
// preset does not exist // preset does not exist
if (isset($this->options['presets'][$preset]) === false) { if (isset($this->options['presets'][$preset]) !== true) {
throw new NotFoundException([ throw new NotFoundException([
'key' => 'email.preset.notFound', 'key' => 'email.preset.notFound',
'data' => ['name' => $preset] 'data' => ['name' => $preset]
@@ -75,6 +90,12 @@ class Email
return $this->options['presets'][$preset]; return $this->options['presets'][$preset];
} }
/**
* Renders the email template(s) and sets the body props
* to the result
*
* @return void
*/
protected function template(): void protected function template(): void
{ {
if (isset($this->props['template']) === true) { 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 $name Template name
* @param string|null $type * @param string|null $type `html` or `text`
* @return \Kirby\Cms\Template * @return \Kirby\Cms\Template
*/ */
protected function getTemplate(string $name, string $type = null) protected function getTemplate(string $name, string $type = null)
@@ -116,47 +137,112 @@ class Email
return App::instance()->template('emails/' . $name, $type, 'text'); return App::instance()->template('emails/' . $name, $type, 'text');
} }
/**
* Returns the prop array
*
* @return array
*/
public function toArray(): array public function toArray(): array
{ {
return $this->props; 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 = $this->props[$prop] ?? [];
// ensure consistent input by making everything an iterable value
if (is_iterable($value) !== true) {
$value = [$value];
}
$result = [];
foreach ($value as $key => $item) {
if (is_string($item) === true) {
// value is already a string // value is already a string
if (is_string($value) === true) { if ($contentKey !== null && is_string($key) === true) {
return $value; $result[$key] = $item;
} else {
$result[] = $item;
} }
} elseif (is_a($item, $class) === true) {
// value is a model object, get value through content method // value is a model object, get value through content method(s)
if (is_a($value, $class) === true) { if ($contentKey !== null) {
return $value->$content(); $result[(string)$item->$contentKey()] = (string)$item->$contentValue();
} else {
$result[] = (string)$item->$contentValue();
} }
} else {
// value is an array or collection, call transform on each item // invalid input
if (is_array($value) === true || is_a($value, 'Kirby\Cms\Collection') === true) { throw new InvalidArgumentException('Invalid input for prop "' . $prop . '", expected string or "' . $class . '" object or collection');
$models = [];
foreach ($value as $model) {
$models[] = $this->transformModel($model, $class, $content);
}
return $models;
} }
} }
protected function transformProp(string $prop, string $model): void 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
{ {
if (isset($this->props[$prop]) === true) { $result = $this->transformModel($addressProp, 'Kirby\Cms\User', 'name', 'email');
$this->props[$prop] = $this->{'transform' . ucfirst($model)}($this->props[$prop]);
$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 transformUser($user) /**
* 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
{ {
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 class FileVersion
{ {
use FileFoundation; use FileFoundation {
toArray as parentToArray;
}
use Properties; use Properties;
protected $modifications; protected $modifications;
@@ -89,7 +91,7 @@ class FileVersion
*/ */
public function toArray(): array public function toArray(): array
{ {
$array = array_merge(parent::toArray(), [ $array = array_merge($this->parentToArray(), [
'modifications' => $this->modifications(), 'modifications' => $this->modifications(),
]); ]);

View File

@@ -51,7 +51,13 @@ trait PageActions
// actually move the page on disk // actually move the page on disk
if ($oldPage->exists() === true) { 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 // overwrite the child in the parent page
@@ -506,10 +512,14 @@ trait PageActions
return $num; return $num;
default: default:
// get instance with default language
$app = $this->kirby()->clone();
$app->setCurrentLanguage();
$template = Str::template($mode, [ $template = Str::template($mode, [
'kirby' => $this->kirby(), 'kirby' => $app,
'page' => $this, 'page' => $app->page($this->id()),
'site' => $this->site(), 'site' => $app->site(),
]); ]);
return (int)$template; return (int)$template;
@@ -706,7 +716,7 @@ trait PageActions
$sibling->changeNum($index); $sibling->changeNum($index);
} }
$parent->children = $siblings->sortBy('num', 'desc'); $parent->children = $siblings->sortBy('num', 'asc');
} }
return true; 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 * @internal
* @param string|null $password * @param string|null $password
@@ -281,12 +282,8 @@ class User extends ModelWithContent
public static function hashPassword($password): ?string public static function hashPassword($password): ?string
{ {
if ($password !== null) { 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; 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 * @param string $password
* @return self * @return self

View File

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

View File

@@ -25,7 +25,9 @@ class Email
protected $bcc; protected $bcc;
protected $cc; protected $cc;
protected $from; protected $from;
protected $fromName;
protected $replyTo; protected $replyTo;
protected $replyToName;
protected $isSent = false; protected $isSent = false;
protected $subject; protected $subject;
protected $to; protected $to;
@@ -75,6 +77,11 @@ class Email
return $this->from; return $this->from;
} }
public function fromName(): ?string
{
return $this->fromName;
}
public function isHtml() public function isHtml()
{ {
return $this->body()->html() !== null; return $this->body()->html() !== null;
@@ -90,6 +97,11 @@ class Email
return $this->replyTo; return $this->replyTo;
} }
public function replyToName(): ?string
{
return $this->replyToName;
}
protected function resolveEmail($email = null, bool $multiple = true) protected function resolveEmail($email = null, bool $multiple = true)
{ {
if ($email === null) { if ($email === null) {
@@ -97,16 +109,27 @@ class Email
} }
if (is_array($email) === false) { 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) { if (V::email($address) === false) {
throw new Exception(sprintf('"%s" is not a valid email address', $address)); 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 public function send(): bool
@@ -148,12 +171,24 @@ class Email
return $this; return $this;
} }
protected function setFromName(string $fromName = null)
{
$this->fromName = $fromName;
return $this;
}
protected function setReplyTo(string $replyTo = null) protected function setReplyTo(string $replyTo = null)
{ {
$this->replyTo = $this->resolveEmail($replyTo, false); $this->replyTo = $this->resolveEmail($replyTo, false);
return $this; return $this;
} }
protected function setReplyToName(string $replyToName = null)
{
$this->replyToName = $replyToName;
return $this;
}
protected function setSubject(string $subject) protected function setSubject(string $subject)
{ {
$this->subject = $subject; $this->subject = $subject;

View File

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

View File

@@ -23,6 +23,7 @@ class Remote
*/ */
public static $defaults = [ public static $defaults = [
'agent' => null, 'agent' => null,
'basicAuth' => null,
'body' => true, 'body' => true,
'data' => [], 'data' => [],
'encoding' => 'utf-8', 'encoding' => 'utf-8',
@@ -170,7 +171,22 @@ class Remote
// add all headers // add all headers
if (empty($this->options['headers']) === false) { 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 // add the user agent

View File

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

View File

@@ -31,7 +31,7 @@ class Mime
'avi' => 'video/x-msvideo', 'avi' => 'video/x-msvideo',
'bmp' => 'image/bmp', 'bmp' => 'image/bmp',
'css' => 'text/css', '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', 'doc' => 'application/msword',
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', 'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',

View File

@@ -512,7 +512,9 @@ final class Mbstring
$offset = 0; $offset = 0;
} elseif ($offset = (int) $offset) { } elseif ($offset = (int) $offset) {
if ($offset < 0) { if ($offset < 0) {
if (0 > $offset += self::mb_strlen($needle)) {
$haystack = self::mb_substr($haystack, 0, $offset, $encoding); $haystack = self::mb_substr($haystack, 0, $offset, $encoding);
}
$offset = 0; $offset = 0;
} else { } else {
$haystack = self::mb_substr($haystack, $offset, 2147483647, $encoding); $haystack = self::mb_substr($haystack, $offset, 2147483647, $encoding);
@@ -532,7 +534,7 @@ final class Mbstring
return null; return null;
} }
if ($split_length < 1) { if (1 > $split_length = (int) $split_length) {
trigger_error('The length of each segment must be greater than zero', E_USER_WARNING); trigger_error('The length of each segment must be greater than zero', E_USER_WARNING);
return false; return false;
@@ -542,6 +544,10 @@ final class Mbstring
$encoding = mb_internal_encoding(); $encoding = mb_internal_encoding();
} }
if ('UTF-8' === $encoding = self::getEncoding($encoding)) {
return preg_split("/(.{{$split_length}})/u", $string, null, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
}
$result = array(); $result = array();
$length = mb_strlen($string, $encoding); $length = mb_strlen($string, $encoding);
@@ -815,11 +821,16 @@ final class Mbstring
return self::$internalEncoding; return self::$internalEncoding;
} }
if ('UTF-8' === $encoding) {
return 'UTF-8';
}
$encoding = strtoupper($encoding); $encoding = strtoupper($encoding);
if ('8BIT' === $encoding || 'BINARY' === $encoding) { if ('8BIT' === $encoding || 'BINARY' === $encoding) {
return 'CP850'; return 'CP850';
} }
if ('UTF8' === $encoding) { if ('UTF8' === $encoding) {
return 'UTF-8'; return 'UTF-8';
} }