Merge branch 'develop' of github.com:getgrav/grav into feature/media

 Conflicts:
	CHANGELOG.md
	composer.lock
	system/src/Grav/Common/Media/Interfaces/MediaObjectInterface.php
This commit is contained in:
Matias Griese 2022-06-29 13:21:29 +03:00
commit d1b565f4a7
36 changed files with 2268 additions and 14 deletions

View File

@ -21,6 +21,13 @@
* Fixed remote URLs in markdown if using subfolder setup
* Fixed calls to undefined `Media` methods, they will now return `null` in order to fix twig templates with undefined variables
# v1.7.35
## mm/dd/2022
1. [](#new)
* Added support for `multipart/form-data` content type in PUT and PATCH requests
* Added support for object relationships
# v1.7.34
## 06/14/2022

View File

@ -62,7 +62,7 @@
},
"require-dev": {
"codeception/codeception": "^4.1",
"phpstan/phpstan": "^1.2",
"phpstan/phpstan": "^1.8",
"phpstan/phpstan-deprecation-rules": "^1.0",
"phpunit/php-code-coverage": "~9.2",
"getgrav/markdowndocs": "^2.0",

View File

@ -1156,6 +1156,13 @@ form:
local6: local6
local7: local7
log.syslog.tag:
type: text
size: small
label: PLUGIN_ADMIN.SYSLOG_TAG
help: PLUGIN_ADMIN.SYSLOG_TAG_HELP
placeholder: "grav"
debugger:
type: tab
title: PLUGIN_ADMIN.DEBUGGER

View File

@ -125,6 +125,17 @@ config:
- username
- fullname
relationships:
media:
type: media
cardinality: to-many
avatar:
type: media
cardinality: to-one
# roles:
# type: user-groups
# cardinality: to-many
blueprints:
configure:
fields:

View File

@ -144,6 +144,7 @@ log:
handler: file # Log handler. Currently supported: file | syslog
syslog:
facility: local6 # Syslog facilities output
tag: grav # Syslog tag. Default: "grav".
debugger:
enabled: false # Enable Grav debugger and following settings

View File

@ -31,6 +31,7 @@ use Grav\Common\Flex\Types\UserGroups\UserGroupIndex;
use Grav\Common\User\Interfaces\UserInterface;
use Grav\Common\User\Traits\UserTrait;
use Grav\Common\Utils;
use Grav\Framework\Contracts\Relationships\ToOneRelationshipInterface;
use Grav\Framework\File\Formatter\JsonFormatter;
use Grav\Framework\File\Formatter\YamlFormatter;
use Grav\Framework\Filesystem\Filesystem;
@ -38,7 +39,10 @@ use Grav\Framework\Flex\Flex;
use Grav\Framework\Flex\FlexDirectory;
use Grav\Framework\Flex\Storage\FileStorage;
use Grav\Framework\Flex\Traits\FlexMediaTrait;
use Grav\Framework\Flex\Traits\FlexRelationshipsTrait;
use Grav\Framework\Form\FormFlashFile;
use Grav\Framework\Media\MediaIdentifier;
use Grav\Framework\Media\UploadedMediaObject;
use Psr\Http\Message\UploadedFileInterface;
use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\File\FileInterface;
@ -77,6 +81,7 @@ class UserObject extends FlexObject implements UserInterface, Countable
}
use UserTrait;
use UserObjectLegacyTrait;
use FlexRelationshipsTrait;
/** @var Closure|null */
static public $authorizeCallable;
@ -682,6 +687,81 @@ class UserObject extends FlexObject implements UserInterface, Countable
return $folder;
}
/**
* @param string $name
* @return array|object|null
* @internal
*/
public function initRelationship(string $name)
{
switch ($name) {
case 'media':
$list = [];
foreach ($this->getMedia()->all() as $filename => $object) {
$list[] = $this->buildMediaObject(null, $filename, $object);
}
return $list;
case 'avatar':
return $this->buildMediaObject('avatar', basename($this->getAvatarUrl()), $this->getAvatarImage());
}
throw new \InvalidArgumentException(sprintf('%s: Relationship %s does not exist', $this->getFlexType(), $name));
}
/**
* @return bool Return true if relationships were updated.
*/
protected function updateRelationships(): bool
{
$modified = $this->getRelationships()->getModified();
if ($modified) {
foreach ($modified as $relationship) {
$name = $relationship->getName();
switch ($name) {
case 'avatar':
\assert($relationship instanceof ToOneRelationshipInterface);
$this->updateAvatarRelationship($relationship);
break;
default:
throw new \InvalidArgumentException(sprintf('%s: Relationship %s cannot be modified', $this->getFlexType(), $name), 400);
}
}
$this->resetRelationships();
return true;
}
return false;
}
/**
* @param ToOneRelationshipInterface $relationship
*/
protected function updateAvatarRelationship(ToOneRelationshipInterface $relationship): void
{
$files = [];
$avatar = $this->getAvatarImage();
if ($avatar) {
$files['avatar'][$avatar->filename] = null;
}
$identifier = $relationship->getIdentifier();
if ($identifier) {
\assert($identifier instanceof MediaIdentifier);
$object = $identifier->getObject();
if ($object instanceof UploadedMediaObject) {
$uploadedFile = $object->getUploadedFile();
if ($uploadedFile) {
$files['avatar'][$uploadedFile->getClientFilename()] = $uploadedFile;
}
}
}
$this->update([], $files);
}
/**
* @param string $name
* @return Blueprint

View File

@ -48,6 +48,7 @@ use Grav\Common\Service\TaskServiceProvider;
use Grav\Common\Twig\Twig;
use Grav\Framework\DI\Container;
use Grav\Framework\Psr7\Response;
use Grav\Framework\RequestHandler\Middlewares\MultipartRequestSupport;
use Grav\Framework\RequestHandler\RequestHandler;
use Grav\Framework\Route\Route;
use Grav\Framework\Session\Messages;
@ -117,6 +118,7 @@ class Grav extends Container
* @var array All middleware processors that are processed in $this->process()
*/
protected $middleware = [
'multipartRequestSupport',
'initializeProcessor',
'pluginsProcessor',
'themesProcessor',
@ -259,6 +261,9 @@ class Grav extends Container
$container = new Container(
[
'multipartRequestSupport' => function () {
return new MultipartRequestSupport();
},
'initializeProcessor' => function () {
return new InitializeProcessor($this);
},

View File

@ -261,7 +261,8 @@ class InitializeProcessor extends ProcessorBase
$log->popHandler();
$facility = $config->get('system.log.syslog.facility', 'local6');
$logHandler = new SyslogHandler('grav', $facility);
$tag = $config->get('system.log.syslog.tag', 'grav');
$logHandler = new SyslogHandler($tag, $facility);
$formatter = new LineFormatter("%channel%.%level_name%: %message% %extra%");
$logHandler->setFormatter($formatter);

View File

@ -0,0 +1,52 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Contracts\Media;
use Grav\Framework\Contracts\Object\IdentifierInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Media Object Interface
*/
interface MediaObjectInterface extends IdentifierInterface
{
/**
* Returns true if the object exists.
*
* @return bool
* @phpstan-pure
*/
public function exists(): bool;
/**
* Get metadata associated to the media object.
*
* @return array
* @phpstan-pure
*/
public function getMeta(): array;
/**
* @param string $field
* @return mixed
* @phpstan-pure
*/
public function get(string $field);
/**
* Return URL pointing to the media object.
*
* @return string
* @phpstan-pure
*/
public function getUrl(): string;
/**
* Create media response.
*
* @param array $actions
* @return ResponseInterface
* @phpstan-pure
*/
public function createResponse(array $actions): ResponseInterface;
}

View File

@ -0,0 +1,27 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Contracts\Object;
use JsonSerializable;
/**
* Interface IdentifierInterface
*/
interface IdentifierInterface extends JsonSerializable
{
/**
* Get identifier's ID.
*
* @return string
* @phpstan-pure
*/
public function getId(): string;
/**
* Get identifier's type.
*
* @return string
* @phpstan-pure
*/
public function getType(): string;
}

View File

@ -0,0 +1,28 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Contracts\Relationships;
use ArrayAccess;
use Grav\Framework\Contracts\Object\IdentifierInterface;
/**
* Interface RelationshipIdentifierInterface
*/
interface RelationshipIdentifierInterface extends IdentifierInterface
{
/**
* If identifier has meta.
*
* @return bool
* @phpstan-pure
*/
public function hasIdentifierMeta(): bool;
/**
* Get identifier meta.
*
* @return array<string,mixed>|ArrayAccess<string,mixed>
* @phpstan-pure
*/
public function getIdentifierMeta();
}

View File

@ -0,0 +1,81 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Contracts\Relationships;
use Countable;
use Grav\Framework\Contracts\Object\IdentifierInterface;
use IteratorAggregate;
use JsonSerializable;
use Serializable;
/**
* Interface Relationship
*
* @template T of IdentifierInterface
* @template P of IdentifierInterface
* @extends IteratorAggregate<string, T>
*/
interface RelationshipInterface extends Countable, IteratorAggregate, JsonSerializable, Serializable
{
/**
* @return string
* @phpstan-pure
*/
public function getName(): string;
/**
* @return string
* @phpstan-pure
*/
public function getType(): string;
/**
* @return bool
* @phpstan-pure
*/
public function isModified(): bool;
/**
* @return string
* @phpstan-pure
*/
public function getCardinality(): string;
/**
* @return P
* @phpstan-pure
*/
public function getParent(): IdentifierInterface;
/**
* @param string $id
* @param string|null $type
* @return bool
* @phpstan-pure
*/
public function has(string $id, string $type = null): bool;
/**
* @param T $identifier
* @return bool
* @phpstan-pure
*/
public function hasIdentifier(IdentifierInterface $identifier): bool;
/**
* @param T $identifier
* @return bool
*/
public function addIdentifier(IdentifierInterface $identifier): bool;
/**
* @param T|null $identifier
* @return bool
*/
public function removeIdentifier(IdentifierInterface $identifier = null): bool;
/**
* @return iterable<T>
*/
public function getIterator(): iterable;
}

View File

@ -0,0 +1,53 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Contracts\Relationships;
use ArrayAccess;
use Countable;
use Iterator;
use JsonSerializable;
/**
* Interface RelationshipsInterface
*
* @template T of \Grav\Framework\Contracts\Object\IdentifierInterface
* @template P of \Grav\Framework\Contracts\Object\IdentifierInterface
* @extends ArrayAccess<string,RelationshipInterface<T,P>>
* @extends Iterator<string,RelationshipInterface<T,P>>
*/
interface RelationshipsInterface extends Countable, ArrayAccess, Iterator, JsonSerializable
{
/**
* @return bool
* @phpstan-pure
*/
public function isModified(): bool;
/**
* @return array
*/
public function getModified(): array;
/**
* @return int
* @phpstan-pure
*/
public function count(): int;
/**
* @param string $offset
* @return RelationshipInterface<T,P>|null
*/
public function offsetGet($offset): ?RelationshipInterface;
/**
* @return RelationshipInterface<T,P>|null
*/
public function current(): ?RelationshipInterface;
/**
* @return string
* @phpstan-pure
*/
public function key(): string;
}

View File

@ -0,0 +1,55 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Contracts\Relationships;
use Grav\Framework\Contracts\Object\IdentifierInterface;
/**
* Interface ToManyRelationshipInterface
*
* @template T of IdentifierInterface
* @template P of IdentifierInterface
* @template-extends RelationshipInterface<T,P>
*/
interface ToManyRelationshipInterface extends RelationshipInterface
{
/**
* @param positive-int $pos
* @return IdentifierInterface|null
*/
public function getNthIdentifier(int $pos): ?IdentifierInterface;
/**
* @param string $id
* @param string|null $type
* @return T|null
* @phpstan-pure
*/
public function getIdentifier(string $id, string $type = null): ?IdentifierInterface;
/**
* @param string $id
* @param string|null $type
* @return T|null
* @phpstan-pure
*/
public function getObject(string $id, string $type = null): ?object;
/**
* @param iterable<T> $identifiers
* @return bool
*/
public function addIdentifiers(iterable $identifiers): bool;
/**
* @param iterable<T> $identifiers
* @return bool
*/
public function replaceIdentifiers(iterable $identifiers): bool;
/**
* @param iterable<T> $identifiers
* @return bool
*/
public function removeIdentifiers(iterable $identifiers): bool;
}

View File

@ -0,0 +1,37 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Contracts\Relationships;
use Grav\Framework\Contracts\Object\IdentifierInterface;
/**
* Interface ToOneRelationshipInterface
*
* @template T of IdentifierInterface
* @template P of IdentifierInterface
* @template-extends RelationshipInterface<T,P>
*/
interface ToOneRelationshipInterface extends RelationshipInterface
{
/**
* @param string|null $id
* @param string|null $type
* @return T|null
* @phpstan-pure
*/
public function getIdentifier(string $id = null, string $type = null): ?IdentifierInterface;
/**
* @param string|null $id
* @param string|null $type
* @return T|null
* @phpstan-pure
*/
public function getObject(string $id = null, string $type = null): ?object;
/**
* @param T|null $identifier
* @return bool
*/
public function replaceIdentifier(IdentifierInterface $identifier = null): bool;
}

View File

@ -275,6 +275,7 @@ class FlexDirectoryForm implements FlexDirectoryFormInterface, JsonSerializable
'unique_id' => $this->getUniqueId(),
'form_name' => $this->getName(),
'folder' => $this->getFlashFolder(),
'id' => $this->getFlashId(),
'directory' => $this->getDirectory()
];

View File

@ -326,6 +326,7 @@ class FlexForm implements FlexObjectFormInterface, JsonSerializable
'unique_id' => $this->getUniqueId(),
'form_name' => $this->getName(),
'folder' => $this->getFlashFolder(),
'id' => $this->getFlashId(),
'object' => $this->getObject()
];

View File

@ -0,0 +1,75 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Flex;
use Grav\Common\Grav;
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
use Grav\Framework\Object\Identifiers\Identifier;
use RuntimeException;
/**
* Interface IdentifierInterface
*
* @template T of FlexObjectInterface
* @extends Identifier<T>
*/
class FlexIdentifier extends Identifier
{
/** @var string */
private $keyField;
/** @var FlexObjectInterface|null */
private $object = null;
/**
* @param FlexObjectInterface $object
* @return FlexIdentifier<T>
*/
public static function createFromObject(FlexObjectInterface $object): FlexIdentifier
{
$instance = new static($object->getKey(), $object->getFlexType(), 'key');
$instance->setObject($object);
return $instance;
}
/**
* IdentifierInterface constructor.
* @param string $id
* @param string $type
* @param string $keyField
*/
public function __construct(string $id, string $type, string $keyField = 'key')
{
parent::__construct($id, $type);
$this->keyField = $keyField;
}
/**
* @return T
*/
public function getObject(): ?FlexObjectInterface
{
if (!isset($this->object)) {
/** @var Flex $flex */
$flex = Grav::instance()['flex'];
$this->object = $flex->getObject($this->getId(), $this->getType(), $this->keyField);
}
return $this->object;
}
/**
* @param T $object
*/
public function setObject(FlexObjectInterface $object): void
{
$type = $this->getType();
if ($type !== $object->getFlexType()) {
throw new RuntimeException(sprintf('Object has to be type %s, %s given', $type, $object->getFlexType()));
}
$this->object = $object;
}
}

View File

@ -22,6 +22,9 @@ use Grav\Framework\Cache\CacheInterface;
use Grav\Framework\Filesystem\Filesystem;
use Grav\Framework\Flex\FlexDirectory;
use Grav\Framework\Form\FormFlashFile;
use Grav\Framework\Media\Interfaces\MediaObjectInterface;
use Grav\Framework\Media\MediaObject;
use Grav\Framework\Media\UploadedMediaObject;
use Psr\Http\Message\UploadedFileInterface;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use RuntimeException;
@ -232,6 +235,55 @@ trait FlexMediaTrait
];
}
/**
* @param string|null $field
* @param string $filename
* @param MediaObjectInterface|null $image
* @return MediaObject|UploadedMediaObject
*/
protected function buildMediaObject(?string $field, string $filename, MediaObjectInterface $image = null)
{
if (!$image) {
$media = $field ? $this->getMediaField($field) : null;
if ($media) {
$image = $media[$filename];
}
}
return new MediaObject($field, $filename, $image, $this);
}
/**
* @param string|null $field
* @return array
*/
protected function buildMediaList(?string $field): array
{
$names = $field ? (array)$this->getNestedProperty($field) : [];
$media = $field ? $this->getMediaField($field) : null;
if (null === $media) {
$media = $this->getMedia();
}
$list = [];
foreach ($names as $key => $val) {
$name = is_string($val) ? $val : $key;
$medium = $media[$name];
if ($medium) {
if ($medium->uploaded_file) {
$upload = $medium->uploaded_file;
$id = $upload instanceof FormFlashFile ? $upload->getId() : "{$field}-{$name}";
$list[] = new UploadedMediaObject($id, $field, $name, $upload);
} else {
$list[] = $this->buildMediaObject($field, $name, $medium);
}
}
}
return $list;
}
/**
* @param array $files
* @return void
@ -310,7 +362,7 @@ trait FlexMediaTrait
$updated = false;
foreach ($this->getUpdatedMedia() as $filename => $upload) {
if (is_array($upload)) {
// Uses new format with [UploadedFileInterface, array].
/** @var array{UploadedFileInterface,array} $upload */
$settings = $upload[1];
if (isset($settings['destination']) && $settings['destination'] === $media->getPath()) {
$upload = $upload[0];
@ -323,6 +375,7 @@ trait FlexMediaTrait
$updated = true;
if ($medium) {
$medium->uploaded = true;
$medium->uploaded_file = $upload;
$media->add($filename, $medium);
} elseif (is_callable([$media, 'hide'])) {
$media->hide($filename);

View File

@ -0,0 +1,61 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Flex\Traits;
use Grav\Framework\Contracts\Relationships\RelationshipInterface;
use Grav\Framework\Contracts\Relationships\RelationshipsInterface;
use Grav\Framework\Flex\FlexIdentifier;
use Grav\Framework\Relationships\Relationships;
/**
* Trait FlexRelationshipsTrait
*/
trait FlexRelationshipsTrait
{
/** @var RelationshipsInterface|null */
private $_relationships = null;
/**
* @return Relationships
*/
public function getRelationships(): Relationships
{
if (!isset($this->_relationships)) {
$blueprint = $this->getBlueprint();
$options = $blueprint->get('config/relationships', []);
$parent = FlexIdentifier::createFromObject($this);
$this->_relationships = new Relationships($parent, $options);
}
return $this->_relationships;
}
/**
* @param string $name
* @return RelationshipInterface|null
*/
public function getRelationship(string $name): ?RelationshipInterface
{
return $this->getRelationships()[$name];
}
protected function resetRelationships(): void
{
$this->_relationships = null;
}
/**
* @param iterable $collection
* @return array
*/
protected function buildFlexIdentifierList(iterable $collection): array
{
$list = [];
foreach ($collection as $object) {
$list[] = FlexIdentifier::createFromObject($object);
}
return $list;
}
}

View File

@ -31,6 +31,8 @@ class FormFlash implements FormFlashInterface
/** @var bool */
protected $exists;
/** @var string */
protected $id;
/** @var string */
protected $sessionId;
/** @var string */
protected $uniqueId;
@ -75,9 +77,12 @@ class FormFlash implements FormFlashInterface
});
}
$this->sessionId = $config['session_id'] ?? 'no-session';
$this->id = $config['id'] ?? '';
$this->sessionId = $config['session_id'] ?? '';
$this->uniqueId = $config['unique_id'] ?? '';
$this->setUser($config['user'] ?? null);
$folder = $config['folder'] ?? ($this->sessionId ? 'tmp://forms/' . $this->sessionId : '');
/** @var UniformResourceLocator $locator */
@ -133,6 +138,14 @@ class FormFlash implements FormFlashInterface
return $data;
}
/**
* @inheritDoc
*/
public function getId(): string
{
return $this->id && $this->uniqueId ? $this->id . '/' . $this->uniqueId : '';
}
/**
* @inheritDoc
*/
@ -390,8 +403,8 @@ class FormFlash implements FormFlashInterface
*/
public function clearFiles()
{
foreach ($this->files as $field => $files) {
foreach ($files as $name => $upload) {
foreach ($this->files as $files) {
foreach ($files as $upload) {
$this->removeTmpFile($upload['tmp_name'] ?? '');
}
}
@ -406,6 +419,7 @@ class FormFlash implements FormFlashInterface
{
return [
'form' => $this->formName,
'id' => $this->getId(),
'unique_id' => $this->uniqueId,
'url' => $this->url,
'user' => $this->user,

View File

@ -28,6 +28,8 @@ use function sprintf;
*/
class FormFlashFile implements UploadedFileInterface, JsonSerializable
{
/** @var string */
private $id;
/** @var string */
private $field;
/** @var bool */
@ -45,6 +47,7 @@ class FormFlashFile implements UploadedFileInterface, JsonSerializable
*/
public function __construct(string $field, array $upload, FormFlash $flash)
{
$this->id = $flash->getId() ?: $flash->getUniqueId();
$this->field = $field;
$this->upload = $upload;
$this->flash = $flash;
@ -107,6 +110,11 @@ class FormFlashFile implements UploadedFileInterface, JsonSerializable
}
}
public function getId(): string
{
return $this->id;
}
/**
* @return string
*/
@ -222,6 +230,7 @@ class FormFlashFile implements UploadedFileInterface, JsonSerializable
public function __debugInfo()
{
return [
'id:private' => $this->id,
'field:private' => $this->field,
'moved:private' => $this->moved,
'upload:private' => $this->upload,

View File

@ -22,6 +22,13 @@ interface FormFlashInterface extends \JsonSerializable
*/
public function __construct($config);
/**
* Get unique form flash id if set.
*
* @return string
*/
public function getId(): string;
/**
* Get session Id associated to this form instance.
*

View File

@ -453,10 +453,10 @@ trait FormTrait
'session_id' => $this->getSessionId(),
'unique_id' => $this->getUniqueId(),
'form_name' => $this->getName(),
'folder' => $this->getFlashFolder()
'folder' => $this->getFlashFolder(),
'id' => $this->getFlashId()
];
$this->flash = new FormFlash($config);
$this->flash->setUrl($grav['uri']->url)->setUser($grav['user'] ?? null);
}
@ -486,7 +486,8 @@ trait FormTrait
'session_id' => $this->getSessionId(),
'unique_id' => $uniqueId,
'form_name' => $name,
'folder' => $this->getFlashFolder()
'folder' => $this->getFlashFolder(),
'id' => $this->getFlashId()
];
$flash = new FormFlash($config);
if ($flash->exists() && $flash->getFormName() === $name) {
@ -614,6 +615,28 @@ trait FormTrait
return strpos($path, '!!') === false ? rtrim($path, '/') : null;
}
/**
* @return string|null
*/
protected function getFlashId(): ?string
{
// Fill template token keys/value pairs.
$dataMap = [
'[FORM_NAME]' => $this->getName(),
'[SESSIONID]' => 'session',
'[USERNAME]' => '!!',
'[USERNAME_OR_SESSIONID]' => '!!',
'[ACCOUNT]' => 'account'
];
$flashLookupFolder = $this->getFlashLookupFolder();
$path = str_replace(array_keys($dataMap), array_values($dataMap), $flashLookupFolder);
// Make sure we only return valid paths.
return strpos($path, '!!') === false ? rtrim($path, '/') : null;
}
/**
* @return string
*/

View File

@ -9,9 +9,39 @@
namespace Grav\Framework\Media\Interfaces;
use Psr\Http\Message\UploadedFileInterface;
/**
* Class implements media object interface.
*
* @property UploadedFileInterface|null $uploaded_file
*/
interface MediaObjectInterface
{
/**
* Returns an array containing the file metadata
*
* @return array
*/
public function getMeta();
/**
* Return URL to file.
*
* @param bool $reset
* @return string
*/
public function url($reset = true);
/**
* Get value by using dot notation for nested arrays/objects.
*
* @example $value = $this->get('this.is.my.nested.variable');
*
* @param string $name Dot separated path to the requested value.
* @param mixed $default Default value (or null).
* @param string|null $separator Separator, defaults to '.'
* @return mixed Value.
*/
public function get($name, $default = null, $separator = null);
}

View File

@ -0,0 +1,150 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Media;
use Grav\Common\Grav;
use Grav\Common\User\Interfaces\UserInterface;
use Grav\Framework\Contracts\Media\MediaObjectInterface;
use Grav\Framework\Flex\Flex;
use Grav\Framework\Flex\FlexFormFlash;
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
use Grav\Framework\Object\Identifiers\Identifier;
/**
* Interface IdentifierInterface
*
* @template T of MediaObjectInterface
* @extends Identifier<T>
*/
class MediaIdentifier extends Identifier
{
/** @var MediaObjectInterface|null */
private $object = null;
/**
* @param MediaObjectInterface $object
* @return MediaIdentifier<T>
*/
public static function createFromObject(MediaObjectInterface $object): MediaIdentifier
{
$instance = new static($object->getId());
$instance->setObject($object);
return $instance;
}
/**
* @param string $id
*/
public function __construct(string $id)
{
parent::__construct($id, 'media');
}
/**
* @return T
*/
public function getObject(): ?MediaObjectInterface
{
if (!isset($this->object)) {
$type = $this->getType();
$id = $this->getId();
$parts = explode('/', $id);
if ($type === 'media' && str_starts_with($id, 'uploads/')) {
array_shift($parts);
[, $folder, $uniqueId, $field, $filename] = $this->findFlash($parts);
$flash = $this->getFlash($folder, $uniqueId);
if ($flash->exists()) {
$uploadedFile = $flash->getFilesByField($field)[$filename] ?? null;
$this->object = UploadedMediaObject::createFromFlash($flash, $field, $filename, $uploadedFile);
}
} else {
$type = array_shift($parts);
$key = array_shift($parts);
$field = array_shift($parts);
$filename = implode('/', $parts);
$flexObject = $this->getFlexObject($type, $key);
if ($flexObject && method_exists($flexObject, 'getMediaField') && method_exists($flexObject, 'getMedia')) {
$media = $field !== 'media' ? $flexObject->getMediaField($field) : $flexObject->getMedia();
$image = null;
if ($media) {
$image = $media[$filename];
}
$this->object = new MediaObject($field, $filename, $image, $flexObject);
}
}
if (!isset($this->object)) {
throw new \RuntimeException(sprintf('Object not found for identifier {type: "%s", id: "%s"}', $type, $id));
}
}
return $this->object;
}
/**
* @param T $object
*/
public function setObject(MediaObjectInterface $object): void
{
$type = $this->getType();
$objectType = $object->getType();
if ($type !== $objectType) {
throw new \RuntimeException(sprintf('Object has to be type %s, %s given', $type, $objectType));
}
$this->object = $object;
}
protected function findFlash(array $parts): ?array
{
$type = array_shift($parts);
if ($type === 'account') {
/** @var UserInterface|null $user */
$user = Grav::instance()['user'] ?? null;
$folder = $user->getMediaFolder();
} else {
$folder = 'tmp://';
}
if (!$folder) {
return null;
}
do {
$part = array_shift($parts);
$folder .= "/{$part}";
} while (!str_starts_with($part, 'flex-'));
$uniqueId = array_shift($parts);
$field = array_shift($parts);
$filename = implode('/', $parts);
return [$type, $folder, $uniqueId, $field, $filename];
}
protected function getFlash(string $folder, string $uniqueId): FlexFormFlash
{
$config = [
'unique_id' => $uniqueId,
'folder' => $folder
];
return new FlexFormFlash($config);
}
protected function getFlexObject(string $type, string $key): ?FlexObjectInterface
{
/** @var Flex $flex */
$flex = Grav::instance()['flex'];
return $flex->getObject($key, $type);
}
}

View File

@ -0,0 +1,215 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Media;
use Grav\Common\Page\Medium\ImageMedium;
use Grav\Framework\Contracts\Media\MediaObjectInterface;
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
use Grav\Framework\Media\Interfaces\MediaObjectInterface as GravMediaObjectInterface;
use Grav\Framework\Psr7\Response;
use Psr\Http\Message\ResponseInterface;
use Throwable;
/**
* Class MediaObject
*/
class MediaObject implements MediaObjectInterface
{
/** @var string */
static public $placeholderImage = 'image://media/thumb.png';
/** @var FlexObjectInterface */
public $object;
/** @var GravMediaObjectInterface|null */
public $media;
/** @var string|null */
private $field;
/** @var string */
private $filename;
/**
* MediaObject constructor.
* @param string|null $field
* @param string $filename
* @param GravMediaObjectInterface|null $media
* @param FlexObjectInterface $object
*/
public function __construct(?string $field, string $filename, ?GravMediaObjectInterface $media, FlexObjectInterface $object)
{
$this->field = $field;
$this->filename = $filename;
$this->media = $media;
$this->object = $object;
}
/**
* @return string
*/
public function getType(): string
{
return 'media';
}
/**
* @return string
*/
public function getId(): string
{
$field = $this->field;
$object = $this->object;
$path = $field ? "/{$field}/" : '/media/';
return $object->getType() . '/' . $object->getKey() . $path . basename($this->filename);
}
/**
* @return bool
*/
public function exists(): bool
{
return $this->media !== null;
}
/**
* @return array
*/
public function getMeta(): array
{
if (!isset($this->media)) {
return [];
}
return $this->media->getMeta();
}
/**
* @param string $field
* @return mixed|null
*/
public function get(string $field)
{
if (!isset($this->media)) {
return null;
}
return $this->media->get($field);
}
/**
* @return string
*/
public function getUrl(): string
{
if (!isset($this->media)) {
return '';
}
return $this->media->url();
}
/**
* Create media response.
*
* @param array $actions
* @return Response
*/
public function createResponse(array $actions): ResponseInterface
{
if (!isset($this->media)) {
return $this->create404Response($actions);
}
$media = $this->media;
if ($actions) {
$media = $this->processMediaActions($media, $actions);
}
// FIXME: This only works for images
if (!$media instanceof ImageMedium) {
throw new \RuntimeException('Not Implemented', 500);
}
$filename = $media->path(false);
$time = filemtime($filename);
$size = filesize($filename);
$body = fopen($filename, 'rb');
$headers = [
'Content-Type' => $media->get('mime'),
'Last-Modified' => gmdate('D, d M Y H:i:s', $time) . ' GMT',
'ETag' => sprintf('%x-%x', $size, $time)
];
return new Response(200, $headers, $body);
}
/**
* Process media actions
*
* @param GravMediaObjectInterface $medium
* @param array $actions
* @return GravMediaObjectInterface
*/
protected function processMediaActions(GravMediaObjectInterface $medium, array $actions): GravMediaObjectInterface
{
// loop through actions for the image and call them
foreach ($actions as $method => $params) {
$matches = [];
if (preg_match('/\[(.*)]/', $params, $matches)) {
$args = [explode(',', $matches[1])];
} else {
$args = explode(',', $params);
}
try {
$medium->{$method}(...$args);
} catch (Throwable $e) {
// Ignore all errors for now and just skip the action.
}
}
return $medium;
}
/**
* @param array $actions
* @return Response
*/
protected function create404Response(array $actions): Response
{
// Display placeholder image.
$filename = static::$placeholderImage;
$time = filemtime($filename);
$size = filesize($filename);
$body = fopen($filename, 'rb');
$headers = [
'Content-Type' => 'image/svg',
'Last-Modified' => gmdate('D, d M Y H:i:s', $time) . ' GMT',
'ETag' => sprintf('%x-%x', $size, $time)
];
return new Response(404, $headers, $body);
}
/**
* @return array
*/
public function jsonSerialize(): array
{
return [
'type' => $this->getType(),
'id' => $this->getId()
];
}
/**
* @return string[]
*/
public function __debugInfo(): array
{
return $this->jsonSerialize();
}
}

View File

@ -0,0 +1,172 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Media;
use Grav\Framework\Contracts\Media\MediaObjectInterface;
use Grav\Framework\Flex\FlexFormFlash;
use Grav\Framework\Form\Interfaces\FormFlashInterface;
use Grav\Framework\Psr7\Response;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UploadedFileInterface;
/**
* Class UploadedMediaObject
*/
class UploadedMediaObject implements MediaObjectInterface
{
/** @var string */
static public $placeholderImage = 'image://media/thumb.png';
/** @var FormFlashInterface */
public $object;
/** @var string */
private $id;
/** @var string|null */
private $field;
/** @var string */
private $filename;
/** @var array */
private $meta;
/** @var UploadedFileInterface|null */
private $uploadedFile;
/**
* @param FlexFormFlash $flash
* @param string|null $field
* @param string $filename
* @param UploadedFileInterface|null $uploadedFile
* @return static
*/
public static function createFromFlash(FlexFormFlash $flash, ?string $field, string $filename, ?UploadedFileInterface $uploadedFile = null)
{
$id = $flash->getId();
return new static($id, $field, $filename, $uploadedFile);
}
/**
* @param string $id
* @param string|null $field
* @param string $filename
* @param UploadedFileInterface|null $uploadedFile
*/
public function __construct(string $id, ?string $field, string $filename, ?UploadedFileInterface $uploadedFile = null)
{
$this->id = $id;
$this->field = $field;
$this->filename = $filename;
$this->uploadedFile = $uploadedFile;
if ($uploadedFile) {
$this->meta = [
'filename' => $uploadedFile->getClientFilename(),
'mime' => $uploadedFile->getClientMediaType(),
'size' => $uploadedFile->getSize()
];
} else {
$this->meta = [];
}
}
/**
* @return string
*/
public function getType(): string
{
return 'media';
}
/**
* @return string
*/
public function getId(): string
{
$id = $this->id;
$field = $this->field;
$path = $field ? "/{$field}/" : '';
return 'uploads/' . $id . $path . basename($this->filename);
}
/**
* @return bool
*/
public function exists(): bool
{
//return $this->uploadedFile !== null;
return false;
}
/**
* @return array
*/
public function getMeta(): array
{
return $this->meta;
}
/**
* @param string $field
* @return mixed|null
*/
public function get(string $field)
{
return $this->meta[$field] ?? null;
}
/**
* @return string
*/
public function getUrl(): string
{
return '';
}
/**
* @return UploadedFileInterface|null
*/
public function getUploadedFile(): ?UploadedFileInterface
{
return $this->uploadedFile;
}
/**
* @param array $actions
* @return Response
*/
public function createResponse(array $actions): ResponseInterface
{
// Display placeholder image.
$filename = static::$placeholderImage;
$time = filemtime($filename);
$size = filesize($filename);
$body = fopen($filename, 'rb');
$headers = [
'Content-Type' => 'image/svg',
'Last-Modified' => gmdate('D, d M Y H:i:s', $time) . ' GMT',
'ETag' => sprintf('%x-%x', $size, $time)
];
return new Response(404, $headers, $body);
}
/**
* @return array
*/
public function jsonSerialize(): array
{
return [
'type' => $this->getType(),
'id' => $this->getId()
];
}
/**
* @return string[]
*/
public function __debugInfo(): array
{
return $this->jsonSerialize();
}
}

View File

@ -0,0 +1,66 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Object\Identifiers;
use Grav\Framework\Contracts\Object\IdentifierInterface;
/**
* Interface IdentifierInterface
*
* @template T of object
*/
class Identifier implements IdentifierInterface
{
/** @var string */
private $id;
/** @var string */
private $type;
/**
* IdentifierInterface constructor.
* @param string $id
* @param string $type
*/
public function __construct(string $id, string $type)
{
$this->id = $id;
$this->type = $type;
}
/**
* @return string
* @phpstan-pure
*/
public function getId(): string
{
return $this->id;
}
/**
* @return string
* @phpstan-pure
*/
public function getType(): string
{
return $this->type;
}
/**
* @return array
*/
public function jsonSerialize(): array
{
return [
'type' => $this->type,
'id' => $this->id
];
}
/**
* @return array
*/
public function __debugInfo(): array
{
return $this->jsonSerialize();
}
}

View File

@ -0,0 +1,217 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Relationships;
use Grav\Framework\Contracts\Object\IdentifierInterface;
use Grav\Framework\Contracts\Relationships\RelationshipInterface;
use Grav\Framework\Contracts\Relationships\RelationshipsInterface;
use Grav\Framework\Flex\FlexIdentifier;
use RuntimeException;
use function count;
/**
* Class Relationships
*
* @template T of \Grav\Framework\Contracts\Object\IdentifierInterface
* @template P of \Grav\Framework\Contracts\Object\IdentifierInterface
* @implements RelationshipsInterface<T,P>
*/
class Relationships implements RelationshipsInterface
{
/** @var P */
protected $parent;
/** @var array */
protected $options;
/** @var RelationshipInterface<T,P>[] */
protected $relationships;
/**
* Relationships constructor.
* @param P $parent
* @param array $options
*/
public function __construct(IdentifierInterface $parent, array $options)
{
$this->parent = $parent;
$this->options = $options;
$this->relationships = [];
}
/**
* @return bool
* @phpstan-pure
*/
public function isModified(): bool
{
return !empty($this->getModified());
}
/**
* @return RelationshipInterface<T,P>[]
* @phpstan-pure
*/
public function getModified(): array
{
$list = [];
foreach ($this->relationships as $name => $relationship) {
if ($relationship->isModified()) {
$list[$name] = $relationship;
}
}
return $list;
}
/**
* @return int
* @phpstan-pure
*/
public function count(): int
{
return count($this->options);
}
/**
* @param string $offset
* @return bool
* @phpstan-pure
*/
public function offsetExists($offset): bool
{
return isset($this->options[$offset]);
}
/**
* @param string $offset
* @return RelationshipInterface<T,P>|null
*/
public function offsetGet($offset): ?RelationshipInterface
{
if (!isset($this->relationships[$offset])) {
$options = $this->options[$offset] ?? null;
if (null === $options) {
return null;
}
$this->relationships[$offset] = $this->createRelationship($offset, $options);
}
return $this->relationships[$offset];
}
/**
* @param string $offset
* @param mixed $value
* @return never-return
*/
public function offsetSet($offset, $value)
{
throw new RuntimeException('Setting relationship is not supported', 500);
}
/**
* @param string $offset
* @return never-return
*/
public function offsetUnset($offset)
{
throw new RuntimeException('Removing relationship is not allowed', 500);
}
/**
* @return RelationshipInterface<T,P>|null
*/
public function current(): ?RelationshipInterface
{
$name = key($this->options);
if ($name === null) {
return null;
}
return $this->offsetGet($name);
}
/**
* @return string
* @phpstan-pure
*/
public function key(): string
{
return key($this->options);
}
/**
* @return void
* @phpstan-pure
*/
public function next(): void
{
next($this->options);
}
/**
* @return void
* @phpstan-pure
*/
public function rewind(): void
{
reset($this->options);
}
/**
* @return bool
* @phpstan-pure
*/
public function valid(): bool
{
return key($this->options) !== null;
}
/**
* @return array
*/
public function jsonSerialize(): array
{
$list = [];
foreach ($this as $name => $relationship) {
$list[$name] = $relationship->jsonSerialize();
}
return $list;
}
/**
* @param string $name
* @param array $options
* @return ToOneRelationship|ToManyRelationship
*/
private function createRelationship(string $name, array $options): RelationshipInterface
{
$data = null;
$parent = $this->parent;
if ($parent instanceof FlexIdentifier) {
$object = $parent->getObject();
if (!method_exists($object, 'initRelationship')) {
throw new RuntimeException(sprintf('Bad relationship %s', $name), 500);
}
$data = $object->initRelationship($name);
}
$cardinality = $options['cardinality'] ?? '';
switch ($cardinality) {
case 'to-one':
$relationship = new ToOneRelationship($parent, $name, $options, $data);
break;
case 'to-many':
$relationship = new ToManyRelationship($parent, $name, $options, $data ?? []);
break;
default:
throw new RuntimeException(sprintf('Bad relationship cardinality %s', $cardinality), 500);
}
return $relationship;
}
}

View File

@ -0,0 +1,259 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Relationships;
use ArrayIterator;
use Grav\Framework\Compat\Serializable;
use Grav\Framework\Contracts\Object\IdentifierInterface;
use Grav\Framework\Contracts\Relationships\ToManyRelationshipInterface;
use Grav\Framework\Relationships\Traits\RelationshipTrait;
use function count;
use function is_callable;
/**
* Class ToManyRelationship
*
* @template T of IdentifierInterface
* @template P of IdentifierInterface
* @template-implements ToManyRelationshipInterface<T,P>
*/
class ToManyRelationship implements ToManyRelationshipInterface
{
/** @template-use RelationshipTrait<T> */
use RelationshipTrait;
use Serializable;
/** @var IdentifierInterface[] */
protected $identifiers = [];
/**
* ToManyRelationship constructor.
* @param string $name
* @param IdentifierInterface $parent
* @param iterable<IdentifierInterface> $identifiers
*/
public function __construct(IdentifierInterface $parent, string $name, array $options, iterable $identifiers = [])
{
$this->parent = $parent;
$this->name = $name;
$this->parseOptions($options);
$this->addIdentifiers($identifiers);
$this->modified = false;
}
/**
* @return string
* @phpstan-pure
*/
public function getCardinality(): string
{
return 'to-many';
}
/**
* @return int
* @phpstan-pure
*/
public function count(): int
{
return count($this->identifiers);
}
/**
* @return array
*/
public function fetch(): array
{
$list = [];
foreach ($this->identifiers as $identifier) {
if (is_callable([$identifier, 'getObject'])) {
$identifier = $identifier->getObject();
}
$list[] = $identifier;
}
return $list;
}
/**
* @param string $id
* @param string|null $type
* @return bool
* @phpstan-pure
*/
public function has(string $id, string $type = null): bool
{
return $this->getIdentifier($id, $type) !== null;
}
/**
* @param positive-int $pos
* @return IdentifierInterface|null
*/
public function getNthIdentifier(int $pos): ?IdentifierInterface
{
$items = array_keys($this->identifiers);
$key = $items[$pos - 1] ?? null;
if (null === $key) {
return null;
}
return $this->identifiers[$key] ?? null;
}
/**
* @param string $id
* @param string|null $type
* @return IdentifierInterface|null
* @phpstan-pure
*/
public function getIdentifier(string $id, string $type = null): ?IdentifierInterface
{
if (null === $type) {
$type = $this->getType();
}
if ($type === 'media' && !str_contains($id, '/')) {
$name = $this->name;
$id = $this->parent->getType() . '/' . $this->parent->getId() . '/'. $name . '/' . $id;
}
$key = "{$type}/{$id}";
return $this->identifiers[$key] ?? null;
}
/**
* @param string $id
* @param string|null $type
* @return T|null
*/
public function getObject(string $id, string $type = null): ?object
{
$identifier = $this->getIdentifier($id, $type);
if ($identifier && is_callable([$identifier, 'getObject'])) {
$identifier = $identifier->getObject();
}
return $identifier;
}
/**
* @param IdentifierInterface $identifier
* @return bool
*/
public function addIdentifier(IdentifierInterface $identifier): bool
{
return $this->addIdentifiers([$identifier]);
}
/**
* @param IdentifierInterface|null $identifier
* @return bool
*/
public function removeIdentifier(IdentifierInterface $identifier = null): bool
{
return !$identifier || $this->removeIdentifiers([$identifier]);
}
/**
* @param iterable<IdentifierInterface> $identifiers
* @return bool
*/
public function addIdentifiers(iterable $identifiers): bool
{
foreach ($identifiers as $identifier) {
$type = $identifier->getType();
$id = $identifier->getId();
$key = "{$type}/{$id}";
$this->identifiers[$key] = $this->checkIdentifier($identifier);
$this->modified = true;
}
return true;
}
/**
* @param iterable<IdentifierInterface> $identifiers
* @return bool
*/
public function replaceIdentifiers(iterable $identifiers): bool
{
$this->identifiers = [];
$this->modified = true;
return $this->addIdentifiers($identifiers);
}
/**
* @param iterable<IdentifierInterface> $identifiers
* @return bool
*/
public function removeIdentifiers(iterable $identifiers): bool
{
foreach ($identifiers as $identifier) {
$type = $identifier->getType();
$id = $identifier->getId();
$key = "{$type}/{$id}";
unset($this->identifiers[$key]);
$this->modified = true;
}
return true;
}
/**
* @return iterable<IdentifierInterface>
* @phpstan-pure
*/
public function getIterator(): iterable
{
return new ArrayIterator($this->identifiers);
}
/**
* @return array
*/
public function jsonSerialize(): array
{
$list = [];
foreach ($this->getIterator() as $item) {
$list[] = $item->jsonSerialize();
}
return $list;
}
/**
* @return array
*/
public function __serialize(): array
{
return [
'parent' => $this->parent,
'name' => $this->name,
'type' => $this->type,
'options' => $this->options,
'modified' => $this->modified,
'identifiers' => $this->identifiers,
];
}
/**
* @param array $data
* @return void
*/
public function __unserialize(array $data): void
{
$this->parent = $data['parent'];
$this->name = $data['name'];
$this->type = $data['type'];
$this->options = $data['options'];
$this->modified = $data['modified'];
$this->identifiers = $data['identifiers'];
}
}

View File

@ -0,0 +1,207 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Relationships;
use ArrayIterator;
use Grav\Framework\Compat\Serializable;
use Grav\Framework\Contracts\Object\IdentifierInterface;
use Grav\Framework\Contracts\Relationships\ToOneRelationshipInterface;
use Grav\Framework\Relationships\Traits\RelationshipTrait;
use function is_callable;
/**
* Class ToOneRelationship
*
* @template T of IdentifierInterface
* @template P of IdentifierInterface
* @template-implements ToOneRelationshipInterface<T,P>
*/
class ToOneRelationship implements ToOneRelationshipInterface
{
/** @template-use RelationshipTrait<T> */
use RelationshipTrait;
use Serializable;
/** @var IdentifierInterface|null */
protected $identifier = null;
public function __construct(IdentifierInterface $parent, string $name, array $options, IdentifierInterface $identifier = null)
{
$this->parent = $parent;
$this->name = $name;
$this->parseOptions($options);
$this->replaceIdentifier($identifier);
$this->modified = false;
}
/**
* @return string
* @phpstan-pure
*/
public function getCardinality(): string
{
return 'to-one';
}
/**
* @return int
* @phpstan-pure
*/
public function count(): int
{
return $this->identifier ? 1 : 0;
}
/**
* @return object|null
*/
public function fetch(): ?object
{
$identifier = $this->identifier;
if (is_callable([$identifier, 'getObject'])) {
$identifier = $identifier->getObject();
}
return $identifier;
}
/**
* @param string|null $id
* @param string|null $type
* @return bool
* @phpstan-pure
*/
public function has(string $id = null, string $type = null): bool
{
return $this->getIdentifier($id, $type) !== null;
}
/**
* @param string|null $id
* @param string|null $type
* @return IdentifierInterface|null
* @phpstan-pure
*/
public function getIdentifier(string $id = null, string $type = null): ?IdentifierInterface
{
if ($id && $this->getType() === 'media' && !str_contains($id, '/')) {
$name = $this->name;
$id = $this->parent->getType() . '/' . $this->parent->getId() . '/'. $name . '/' . $id;
}
$identifier = $this->identifier ?? null;
if (null === $identifier || ($type && $type !== $identifier->getType()) || ($id && $id !== $identifier->getId())) {
return null;
}
return $identifier;
}
/**
* @param string|null $id
* @param string|null $type
* @return T|null
*/
public function getObject(string $id = null, string $type = null): ?object
{
$identifier = $this->getIdentifier($id, $type);
if ($identifier && is_callable([$identifier, 'getObject'])) {
$identifier = $identifier->getObject();
}
return $identifier;
}
/**
* @param IdentifierInterface $identifier
* @return bool
*/
public function addIdentifier(IdentifierInterface $identifier): bool
{
$this->identifier = $this->checkIdentifier($identifier);
$this->modified = true;
return true;
}
/**
* @param IdentifierInterface|null $identifier
* @return bool
*/
public function replaceIdentifier(IdentifierInterface $identifier = null): bool
{
if ($identifier === null) {
$this->identifier = null;
$this->modified = true;
return true;
}
return $this->addIdentifier($identifier);
}
/**
* @param IdentifierInterface|null $identifier
* @return bool
*/
public function removeIdentifier(IdentifierInterface $identifier = null): bool
{
if (null === $identifier || $this->has($identifier->getId(), $identifier->getType())) {
$this->identifier = null;
$this->modified = true;
return true;
}
return false;
}
/**
* @return iterable<IdentifierInterface>
* @phpstan-pure
*/
public function getIterator(): iterable
{
return new ArrayIterator((array)$this->identifier);
}
/**
* @return array|null
*/
public function jsonSerialize(): ?array
{
return $this->identifier ? $this->identifier->jsonSerialize() : null;
}
/**
* @return array
*/
public function __serialize(): array
{
return [
'parent' => $this->parent,
'name' => $this->name,
'type' => $this->type,
'options' => $this->options,
'modified' => $this->modified,
'identifier' => $this->identifier,
];
}
/**
* @param array $data
* @return void
*/
public function __unserialize(array $data): void
{
$this->parent = $data['parent'];
$this->name = $data['name'];
$this->type = $data['type'];
$this->options = $data['options'];
$this->modified = $data['modified'];
$this->identifier = $data['identifier'];
}
}

View File

@ -0,0 +1,128 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Relationships\Traits;
use Grav\Framework\Contracts\Object\IdentifierInterface;
use Grav\Framework\Flex\FlexIdentifier;
use Grav\Framework\Media\MediaIdentifier;
use Grav\Framework\Object\Identifiers\Identifier;
use RuntimeException;
use function get_class;
/**
* Trait RelationshipTrait
*
* @template T of object
*/
trait RelationshipTrait
{
/** @var IdentifierInterface */
protected $parent;
/** @var string */
protected $name;
/** @var string */
protected $type;
/** @var array */
protected $options;
/** @var bool */
protected $modified = false;
/**
* @return string
* @phpstan-pure
*/
public function getName(): string
{
return $this->name;
}
/**
* @return string
* @phpstan-pure
*/
public function getType(): string
{
return $this->type;
}
/**
* @return bool
* @phpstan-pure
*/
public function isModified(): bool
{
return $this->modified;
}
/**
* @return IdentifierInterface
* @phpstan-pure
*/
public function getParent(): IdentifierInterface
{
return $this->parent;
}
/**
* @param IdentifierInterface $identifier
* @return bool
* @phpstan-pure
*/
public function hasIdentifier(IdentifierInterface $identifier): bool
{
return $this->getIdentifier($identifier->getId(), $identifier->getType()) !== null;
}
/**
* @return int
* @phpstan-pure
*/
abstract public function count(): int;
/**
* @return void
* @phpstan-pure
*/
public function check(): void
{
$min = $this->options['min'] ?? 0;
$max = $this->options['max'] ?? 0;
if ($min || $max) {
$count = $this->count();
if ($min && $count < $min) {
throw new RuntimeException(sprintf('%s relationship has too few objects in it', $this->name));
}
if ($max && $count > $max) {
throw new RuntimeException(sprintf('%s relationship has too many objects in it', $this->name));
}
}
}
/**
* @param IdentifierInterface $identifier
* @return IdentifierInterface
*/
private function checkIdentifier(IdentifierInterface $identifier): IdentifierInterface
{
if ($this->type !== $identifier->getType()) {
throw new RuntimeException(sprintf('Bad identifier type %s', $identifier->getType()));
}
if (get_class($identifier) !== Identifier::class) {
return $identifier;
}
if ($this->type === 'media') {
return new MediaIdentifier($identifier->getId());
}
return new FlexIdentifier($identifier->getId(), $identifier->getType());
}
private function parseOptions(array $options): void
{
$this->type = $options['type'];
$this->options = $options;
}
}

View File

@ -1,4 +1,4 @@
<?php
<?php declare(strict_types=1);
/**
* @package Grav\Framework\RequestHandler
@ -7,8 +7,6 @@
* @license MIT License; see LICENSE file for details.
*/
declare(strict_types=1);
namespace Grav\Framework\RequestHandler\Middlewares;
use Grav\Common\Data\ValidationException;

View File

@ -0,0 +1,123 @@
<?php declare(strict_types=1);
/**
* @package Grav\Framework\RequestHandler
*
* @copyright Copyright (c) 2015 - 2022 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\RequestHandler\Middlewares;
use Grav\Framework\Psr7\UploadedFile;
use Nyholm\Psr7\Stream;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\UploadedFileInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use function array_slice;
use function count;
use function in_array;
use function is_array;
use function strlen;
/**
* Multipart request support for PUT and PATCH.
*/
class MultipartRequestSupport implements MiddlewareInterface
{
/**
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
* @return ResponseInterface
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$contentType = $request->getHeaderLine('content-type');
$method = $request->getMethod();
if (!str_starts_with($contentType, 'multipart/form-data') || !in_array($method, ['PUT', 'PATH'], true)) {
return $handler->handle($request);
}
$boundary = explode('; boundary=', $contentType, 2)[1] ?? '';
$parts = explode("--{$boundary}", $request->getBody()->getContents());
$parts = array_slice($parts, 1, count($parts) - 2);
$params = [];
$files = [];
foreach ($parts as $part) {
$this->processPart($params, $files, $part);
}
return $handler->handle($request->withParsedBody($params)->withUploadedFiles($files));
}
/**
* @param array $params
* @param array $files
* @param string $part
* @return void
*/
protected function processPart(array &$params, array &$files, string $part): void
{
$part = ltrim($part, "\r\n");
[$rawHeaders, $body] = explode("\r\n\r\n", $part, 2);
// Parse headers.
$rawHeaders = explode("\r\n", $rawHeaders);
$headers = array_reduce(
$rawHeaders,
static function (array $headers, $header) {
[$name, $value] = explode(':', $header);
$headers[strtolower($name)] = ltrim($value, ' ');
return $headers;
},
[]
);
if (!isset($headers['content-disposition'])) {
return;
}
// Parse content disposition header.
$contentDisposition = $headers['content-disposition'];
preg_match('/^(.+); *name="([^"]+)"(; *filename="([^"]+)")?/', $contentDisposition, $matches);
$name = $matches[2];
$filename = $matches[4] ?? null;
if ($filename !== null) {
$stream = Stream::create($body);
$this->addFile($files, $name, new UploadedFile($stream, strlen($body), UPLOAD_ERR_OK, $filename, $headers['content-type'] ?? null));
} elseif (strpos($contentDisposition, 'filename') !== false) {
// Not uploaded file.
$stream = Stream::create('');
$this->addFile($files, $name, new UploadedFile($stream, 0, UPLOAD_ERR_NO_FILE));
} else {
// Regular field.
$params[$name] = substr($body, 0, -2);
}
}
/**
* @param array $files
* @param string $name
* @param UploadedFileInterface $file
* @return void
*/
protected function addFile(array &$files, string $name, UploadedFileInterface $file): void
{
if (strpos($name, '[]') === strlen($name) - 2) {
$name = substr($name, 0, -2);
if (isset($files[$name]) && is_array($files[$name])) {
$files[$name][] = $file;
} else {
$files[$name] = [$file];
}
} else {
$files[$name] = $file;
}
}
}

View File

@ -28,10 +28,10 @@ trait RequestHandlerTrait
protected $middleware;
/** @var callable */
private $handler;
protected $handler;
/** @var ContainerInterface|null */
private $container;
protected $container;
/**
* {@inheritdoc}