mirror of
https://github.com/humhub/humhub.git
synced 2025-01-17 22:28:51 +01:00
Enh: file
fields: metadata
(#6594)
* Add `File.metadata` * Mark experimental * fixup! Add `File.metadata` * Use `isModified()` rather than `count` to evaluate `null` before saving.
This commit is contained in:
parent
3d107cb4de
commit
06d5114cde
@ -3,6 +3,7 @@ HumHub Changelog
|
|||||||
|
|
||||||
1.15.0-beta.2 (Unreleased)
|
1.15.0-beta.2 (Unreleased)
|
||||||
--------------------------
|
--------------------------
|
||||||
|
- Enh #6594: Add field `file.metadata`
|
||||||
- Enh #6593: Add field `file.sort_order`
|
- Enh #6593: Add field `file.sort_order`
|
||||||
- Enh #6592: Add field `file.state`
|
- Enh #6592: Add field `file.state`
|
||||||
- Enh #6591: Add field `file.category`
|
- Enh #6591: Add field `file.category`
|
||||||
|
@ -52,3 +52,19 @@ class __Application {
|
|||||||
class __WebApplication extends \humhub\components\Application
|
class __WebApplication extends \humhub\components\Application
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!class_exists(WeakReference::class)) {
|
||||||
|
class WeakReference
|
||||||
|
{
|
||||||
|
/* Methods */
|
||||||
|
public static function create(object $object): self
|
||||||
|
{
|
||||||
|
return new static();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get(): ?object
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -98,9 +98,11 @@ trait InvalidArgumentExceptionTrait
|
|||||||
$int = filter_var($this->parameter, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
$int = filter_var($this->parameter, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
||||||
|
|
||||||
if ($int === null) {
|
if ($int === null) {
|
||||||
|
$parameter = preg_replace('@^(\.\.\.)?\$?@', '$1\$', $this->parameter);
|
||||||
|
|
||||||
return $this->parameter === null
|
return $this->parameter === null
|
||||||
? 'Unknown argument'
|
? 'Unknown argument'
|
||||||
: "Argument \$" . ltrim($this->parameter, '$');
|
: "Argument " . $parameter;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'Argument #' . $int;
|
return 'Argument #' . $int;
|
||||||
|
907
protected/humhub/libs/StdClass.php
Normal file
907
protected/humhub/libs/StdClass.php
Normal file
@ -0,0 +1,907 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @link https://www.humhub.org/
|
||||||
|
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
|
||||||
|
* @license https://www.humhub.com/licences
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace humhub\libs;
|
||||||
|
|
||||||
|
use __PHP_Incomplete_Class;
|
||||||
|
use ArrayAccess;
|
||||||
|
use ArrayIterator;
|
||||||
|
use Countable;
|
||||||
|
use humhub\exceptions\InvalidArgumentTypeException;
|
||||||
|
use humhub\exceptions\InvalidArgumentValueException;
|
||||||
|
use humhub\exceptions\InvalidConfigTypeException;
|
||||||
|
use OutOfBoundsException;
|
||||||
|
use ReflectionClass;
|
||||||
|
use ReflectionException;
|
||||||
|
use ReflectionObject;
|
||||||
|
use ReflectionProperty;
|
||||||
|
use SeekableIterator;
|
||||||
|
use Serializable;
|
||||||
|
use Stringable;
|
||||||
|
use Traversable;
|
||||||
|
use Yii;
|
||||||
|
use yii\base\Arrayable;
|
||||||
|
use yii\base\ArrayableTrait;
|
||||||
|
use yii\helpers\ArrayHelper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WARNING: This class and its API is still in experimental state. Expect changes in 1.16 (ToDo)
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* This class provides an object that can have dynamic properties (like \stdClass) but with some additional features:
|
||||||
|
* - the properties can also be access in the array-like manner, \
|
||||||
|
* i.e. `$object['property']` for `$object->property`
|
||||||
|
* - `count()` can be used to get the number of properties
|
||||||
|
* - the object can be cleared \
|
||||||
|
* with `static:clear()` or `static::addValue(null)`
|
||||||
|
* - add multiple values in as an array \
|
||||||
|
* with `static::addValue($property1 => $value1, $property2 => $value2, ...)`
|
||||||
|
* - add multiple array of value sets which will be merged first \
|
||||||
|
* with `static::addValue([$property1 => $value1, ...], [$property2 => $value2, ...], ...)`
|
||||||
|
* - allows to verify if the entire object or a particular property has been modified \
|
||||||
|
* with `static::isModified()` \
|
||||||
|
* and `static::isFieldModified($field)`
|
||||||
|
* - allows to retrieve the (un)modified fields \
|
||||||
|
* with `static::fieldsModified(true|false)` (`null` will return all fields)
|
||||||
|
* - the internal array pointer can be moved \
|
||||||
|
* with `static::seek($pos)`
|
||||||
|
* - the object is stringable (which by default uses serialize)
|
||||||
|
* - provides a factory
|
||||||
|
* with `\humhub\libs\StdClass::create()`
|
||||||
|
* - the serialization is optimized to
|
||||||
|
* - reduce data by only exporting modified versions of self, and using pre-defined short property keys
|
||||||
|
* - provide format versioning
|
||||||
|
* - only allow deserialization of self and subclasses of self
|
||||||
|
*
|
||||||
|
* @since 1.15 This class and its API is still in experimental state. Expect changes in 1.16 (ToDo)
|
||||||
|
* @internal (ToDo)
|
||||||
|
* @see static::addValues()
|
||||||
|
* @see static::__serialize()
|
||||||
|
* @see static::unserialize()
|
||||||
|
*/
|
||||||
|
class StdClass extends \stdClass implements ArrayAccess, Stringable, SeekableIterator, Countable, Serializable, Arrayable
|
||||||
|
{
|
||||||
|
use ArrayableTrait;
|
||||||
|
|
||||||
|
public const SERIALIZE_FORMAT = 1;
|
||||||
|
|
||||||
|
protected const SERIALIZE_VALUE__VERSION = 'v';
|
||||||
|
protected const SERIALIZE_VALUE__DATA = '_0';
|
||||||
|
/**
|
||||||
|
* List of extracted properties from the object to be unserialized.
|
||||||
|
* - For optional properties, just add the property name to the list
|
||||||
|
* - For *required* properties, use the following syntax: `[$propertyName => true]`
|
||||||
|
*/
|
||||||
|
protected const UNSERIALIZE_REQUIRED_VALUES = [
|
||||||
|
self::SERIALIZE_VALUE__VERSION => true,
|
||||||
|
self::SERIALIZE_VALUE__DATA,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \stdClass|null
|
||||||
|
*/
|
||||||
|
protected static ?\stdClass $validatedObject = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array|traversable|string|null ...$args see class description
|
||||||
|
*
|
||||||
|
* @return static
|
||||||
|
* @throws InvalidConfigTypeException
|
||||||
|
* @see \humhub\libs\StdClass
|
||||||
|
* @noinspection MagicMethodsValidityInspection
|
||||||
|
* @noinspection PhpUnnecessaryFullyQualifiedNameInspection
|
||||||
|
*/
|
||||||
|
public function __construct(...$args)
|
||||||
|
{
|
||||||
|
return $this->addValues(...$args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __get($name)
|
||||||
|
{
|
||||||
|
$this->validatePropertyName($name, __METHOD__);
|
||||||
|
|
||||||
|
/** @noinspection PhpExpressionAlwaysNullInspection */
|
||||||
|
return $this->$name ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function isSerializing(?self $object, bool $end = false): bool
|
||||||
|
{
|
||||||
|
static $current;
|
||||||
|
|
||||||
|
if ($current === null) {
|
||||||
|
if ($object) {
|
||||||
|
$current = $object;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($end && $object && $object === $current) {
|
||||||
|
$current = null;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $name
|
||||||
|
* @param $value
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function __set($name, $value)
|
||||||
|
{
|
||||||
|
$this->validatePropertyName($name, __METHOD__);
|
||||||
|
|
||||||
|
$this->$name = $value;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __isset($name)
|
||||||
|
{
|
||||||
|
return property_exists($this, $name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __unset($name)
|
||||||
|
{
|
||||||
|
$name = $this->validatePropertyName($name, __METHOD__);
|
||||||
|
|
||||||
|
unset($this->$name);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString()
|
||||||
|
{
|
||||||
|
return serialize($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __serialize(): array
|
||||||
|
{
|
||||||
|
$return = [
|
||||||
|
self::SERIALIZE_VALUE__VERSION => self::SERIALIZE_FORMAT,
|
||||||
|
];
|
||||||
|
|
||||||
|
$fields = $this->fieldsModified(true);
|
||||||
|
|
||||||
|
if ($fields !== null) {
|
||||||
|
$fields = array_fill_keys($fields, null);
|
||||||
|
|
||||||
|
foreach ($fields as $field => &$value) {
|
||||||
|
$value = $this->$field;
|
||||||
|
}
|
||||||
|
unset($value);
|
||||||
|
|
||||||
|
$return[self::SERIALIZE_VALUE__DATA] = &$fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array|\stdClass $serialized
|
||||||
|
*
|
||||||
|
* @return self
|
||||||
|
* @throws InvalidArgumentTypeException|InvalidConfigTypeException
|
||||||
|
* @noinspection MagicMethodsValidityInspection
|
||||||
|
*/
|
||||||
|
public function __unserialize($serialized)
|
||||||
|
{
|
||||||
|
$valid = $this->validateSerializedInput($serialized);
|
||||||
|
|
||||||
|
// clear only after validation was successful
|
||||||
|
$this->clear();
|
||||||
|
|
||||||
|
if (!$valid || !property_exists($serialized, self::SERIALIZE_VALUE__DATA)) {
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->addValues($serialized->{self::SERIALIZE_VALUE__DATA});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see static::__serialize()
|
||||||
|
*/
|
||||||
|
public function serialize(): string
|
||||||
|
{
|
||||||
|
return serialize($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function is automatically called when you run `unserialize($string)' (where `$string` includes this class)
|
||||||
|
* or
|
||||||
|
* - `new static($string)`
|
||||||
|
* - `$object->unserialize($string)`
|
||||||
|
* In any case, `$string` MUST start with the object definition of this class and MUST NOT contain any object other
|
||||||
|
* than this class (self::class) or `\stdClass`
|
||||||
|
*
|
||||||
|
* @param string $serialized
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
* @throws InvalidConfigTypeException
|
||||||
|
*/
|
||||||
|
public function unserialize($serialized): self
|
||||||
|
{
|
||||||
|
if (!is_string($serialized)) {
|
||||||
|
throw new InvalidArgumentTypeException(
|
||||||
|
'$serialized',
|
||||||
|
["string"],
|
||||||
|
$serialized
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If `self::class` and `static::class` are the same, they are merged into one key. That's intentional.
|
||||||
|
*
|
||||||
|
* @noinspection PhpDuplicateArrayKeysInspection
|
||||||
|
*/
|
||||||
|
$allowedClasses = [
|
||||||
|
self::class => self::class,
|
||||||
|
static::class => static::class,
|
||||||
|
\stdClass::class => null,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string|null $signature Will contain the signature found for the top-level object
|
||||||
|
*
|
||||||
|
* @noinspection AlterInForeachInspection
|
||||||
|
*/
|
||||||
|
foreach ($allowedClasses as $class => &$signature) {
|
||||||
|
if ($signature === null) {
|
||||||
|
$allowedClasses = array_filter($allowedClasses);
|
||||||
|
throw new InvalidArgumentValueException(
|
||||||
|
'$serialized',
|
||||||
|
sprintf("string starting with '%s'", implode(' or ', $allowedClasses)),
|
||||||
|
$serialized
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$signature = sprintf('O:%s:"%s"', strlen($class), $class);
|
||||||
|
|
||||||
|
if (str_starts_with($serialized, $signature)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace the definition of the top-level object to be a \stdClass, so we can internally unserialize it and
|
||||||
|
// retrieve the values needed to set up this instance
|
||||||
|
$serialized = substr_replace($serialized, 'O:8:"stdClass"', 0, strlen($signature));
|
||||||
|
|
||||||
|
// get the basic values of the serialized object
|
||||||
|
$clone = unserialize($serialized, ['allowed_classes' => array_keys($allowedClasses)]);
|
||||||
|
|
||||||
|
// now translate that into the real structure
|
||||||
|
return $this->__unserialize($clone);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return static
|
||||||
|
* @throws InvalidConfigTypeException
|
||||||
|
* @see static::__construct()
|
||||||
|
* @noinspection PhpParamsInspection
|
||||||
|
* @noinspection PhpUnnecessaryFullyQualifiedNameInspection
|
||||||
|
*/
|
||||||
|
public static function create(...$args): self
|
||||||
|
{
|
||||||
|
return new static(...$args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function clear(): self
|
||||||
|
{
|
||||||
|
foreach ((new ReflectionObject($this))->getProperties(ReflectionProperty::IS_PUBLIC) as $property) {
|
||||||
|
unset($this->{$property->getName()});
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method allows modifying the values of the current object:
|
||||||
|
* - `static::addValue()`: nothing happens.
|
||||||
|
* This is useful for the case where you do `static::addValue(...$someArray)`, where `$someArray` is an empty
|
||||||
|
* array.
|
||||||
|
* - `static::addValue(null)`: This resets the entire set of stored values!
|
||||||
|
* This is equivalent to calling `static::clear()`
|
||||||
|
* - `static::addValue($serializedString)`: Initializes the object with values from `$serializedString`.
|
||||||
|
* This is equivalent to calling `static::unserialize()`
|
||||||
|
* - `static::addValue(array|Traversable $set_1, ...)`:
|
||||||
|
* Any number of arrays or objects implementing `\Traversable` - allowing `foreach($argument as $property =>
|
||||||
|
* $value)`. If more than one set is provided, they are merged according to \`yii\helpers\BaseArrayHelper::merge()`
|
||||||
|
* (earlier values taking precedence), e.g.:
|
||||||
|
* - `static::addValue([$property => $value, ...])`
|
||||||
|
* - `static::addValue($traversableObject)`
|
||||||
|
*
|
||||||
|
* @param ...$args
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
* @throws InvalidArgumentTypeException|InvalidConfigTypeException
|
||||||
|
* @see static::unserialize();
|
||||||
|
* @see \yii\helpers\BaseArrayHelper::merge();
|
||||||
|
* @see static::clear()
|
||||||
|
*/
|
||||||
|
public function addValues(...$args): self
|
||||||
|
{
|
||||||
|
switch (count($args)) {
|
||||||
|
case 0:
|
||||||
|
return $this;
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
$args = reset($args);
|
||||||
|
|
||||||
|
if ($args === null) {
|
||||||
|
return $this->clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_string($args)) {
|
||||||
|
return $this->unserialize($args);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$args = ArrayHelper::merge(...$args);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_iterable($args)) {
|
||||||
|
if (is_object($args)) {
|
||||||
|
$args = ArrayHelper::toArray($args, [], false);
|
||||||
|
} else {
|
||||||
|
throw new InvalidArgumentTypeException('...$args', [
|
||||||
|
'array',
|
||||||
|
Traversable::class
|
||||||
|
], $args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($args as $k => $v) {
|
||||||
|
$this->$k = $v;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function count(): int
|
||||||
|
{
|
||||||
|
return count($this->fields());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isModified(): bool
|
||||||
|
{
|
||||||
|
return $this->fieldsModified(false, true) === null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isFieldModified(string $field): ?bool
|
||||||
|
{
|
||||||
|
if (!$this->__isset($field)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$property = new ReflectionProperty($this, $field);
|
||||||
|
} catch (ReflectionException $e) {
|
||||||
|
// Not an actual property. It may be the result of a getField() getter. So assume, it has been set.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the property has been defined in the class, or dynamically
|
||||||
|
if (!$property->isDefault()) {
|
||||||
|
// we always assume dynamically assigned properties as changed
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// static properties are ignored
|
||||||
|
if ($property->isStatic()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $this->$field;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* this only works as of PHP v8
|
||||||
|
*
|
||||||
|
* @ToDo remove version check and this comment when when min required version is 8.0
|
||||||
|
* @noinspection PhpUndefinedMethodInspection
|
||||||
|
*/
|
||||||
|
if (PHP_MAJOR_VERSION >= 8 && $property->hasDefaultValue() && $data === $property->getDefaultValue()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($data instanceof self) {
|
||||||
|
return $data->isModified();
|
||||||
|
}
|
||||||
|
|
||||||
|
// better be safe than sorry
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param bool|null $filter If null, both modified and unmodified fields are returned as `[$name => $modified]`
|
||||||
|
* pairs. If `true`, only modified fields will be returned. The field names are the array values! If `false`,
|
||||||
|
* only unmodified fields will be returned. The field names are the array values!
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function &fieldsModified(?bool $filter = null, bool $failOnNoMatch = false): ?array
|
||||||
|
{
|
||||||
|
$fields = $this->fields();
|
||||||
|
|
||||||
|
foreach ($fields as $field => &$status) {
|
||||||
|
$status = $this->isFieldModified($field);
|
||||||
|
|
||||||
|
if ($failOnNoMatch && $filter !== null && $status !== $filter) {
|
||||||
|
$fields = null;
|
||||||
|
|
||||||
|
return $fields;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unset($status);
|
||||||
|
|
||||||
|
$fields = array_filter($fields, static fn($value) => $filter === null ? $value !== null : $value === $filter);
|
||||||
|
$fields = count($fields) ? $fields : null;
|
||||||
|
|
||||||
|
if ($fields !== null && $filter !== null) {
|
||||||
|
$fields = array_keys($fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getIterator(): ArrayIterator
|
||||||
|
{
|
||||||
|
return new ArrayIterator(get_object_vars($this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
* @noinspection PhpParamsInspection
|
||||||
|
*/
|
||||||
|
public function current()
|
||||||
|
{
|
||||||
|
return current($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
* @return string|int|null
|
||||||
|
* @noinspection PhpParamsInspection
|
||||||
|
*/
|
||||||
|
public function key()
|
||||||
|
{
|
||||||
|
return key($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
* @noinspection PhpParamsInspection
|
||||||
|
*/
|
||||||
|
public function next()
|
||||||
|
{
|
||||||
|
next($this);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function offsetExists($offset): bool
|
||||||
|
{
|
||||||
|
return $this->__isset($offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function offsetGet($offset)
|
||||||
|
{
|
||||||
|
return $this->__get(
|
||||||
|
$this->validatePropertyName($offset, __METHOD__, '$offset')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function offsetSet($offset, $value)
|
||||||
|
{
|
||||||
|
return $this->__set(
|
||||||
|
$this->validatePropertyName($offset, __METHOD__, '$offset'),
|
||||||
|
$value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function offsetUnset($offset)
|
||||||
|
{
|
||||||
|
return $this->__unset(
|
||||||
|
$this->validatePropertyName($offset, __METHOD__, '$offset')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
* @return static
|
||||||
|
* @noinspection PhpParamsInspection
|
||||||
|
* @noinspection PhpMissingReturnTypeInspection
|
||||||
|
*/
|
||||||
|
public function rewind()
|
||||||
|
{
|
||||||
|
reset($this);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
* @return string|int the current key after seek
|
||||||
|
* @noinspection PhpParamsInspection
|
||||||
|
*/
|
||||||
|
public function seek($position)
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
!is_int($int = $position) && null === $int = filter_var(
|
||||||
|
$position,
|
||||||
|
FILTER_VALIDATE_BOOLEAN,
|
||||||
|
FILTER_NULL_ON_FAILURE
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new InvalidArgumentTypeException('$position', 'int', $position);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($int < 0) {
|
||||||
|
$count = $this->count();
|
||||||
|
$int = $count + $int;
|
||||||
|
|
||||||
|
if ($int < 0) {
|
||||||
|
throw new OutOfBoundsException("Seek position $int is out of range");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reset($this);
|
||||||
|
|
||||||
|
while ($int-- > 0) {
|
||||||
|
next($this);
|
||||||
|
|
||||||
|
if (null === key($this)) {
|
||||||
|
throw new OutOfBoundsException("Seek position $int is out of range");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return key($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
* @noinspection PhpParamsInspection
|
||||||
|
*/
|
||||||
|
public function valid(): bool
|
||||||
|
{
|
||||||
|
return key($this) !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function is used internally to validate property names
|
||||||
|
*
|
||||||
|
* @param $name
|
||||||
|
* @param string $method
|
||||||
|
* @param string|array $parameter
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function validatePropertyName($name, string $method, string $parameter = '$name'): ?string
|
||||||
|
{
|
||||||
|
switch (true) {
|
||||||
|
case is_string($name):
|
||||||
|
case is_int($name):
|
||||||
|
case $name instanceof Stringable:
|
||||||
|
return $name;
|
||||||
|
|
||||||
|
case is_bool($name):
|
||||||
|
return (int)$name;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw InvalidArgumentTypeException::newInstance(
|
||||||
|
$parameter,
|
||||||
|
['string', 'int', 'bool', Stringable::class]
|
||||||
|
)->setMethodName($method);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array|\stdClass $serialized
|
||||||
|
*
|
||||||
|
* @return bool|null True if valid array, False if valid object, null on error
|
||||||
|
* @throws InvalidConfigTypeException
|
||||||
|
*/
|
||||||
|
protected function validateSerializedInput(&$serialized, ?array $requiredFields = self::UNSERIALIZE_REQUIRED_VALUES, bool $throw = true): ?bool
|
||||||
|
{
|
||||||
|
// this is used to identify already-validated data
|
||||||
|
self::$validatedObject ??= self::validatedObject();
|
||||||
|
|
||||||
|
if ($serialized instanceof self::$validatedObject) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_array($serialized)) {
|
||||||
|
$isObject = false;
|
||||||
|
} elseif (!is_object($serialized) || get_class($serialized) !== \stdClass::class) {
|
||||||
|
if (!$throw) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidArgumentTypeException(
|
||||||
|
'$serialized',
|
||||||
|
['array', \stdClass::class],
|
||||||
|
$serialized
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$isObject = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$requiredFields ??= static::UNSERIALIZE_REQUIRED_VALUES;
|
||||||
|
|
||||||
|
// check if the default version field (v) is explicitly not wanted
|
||||||
|
if (($requiredFields[self::SERIALIZE_VALUE__VERSION] ?? true) === null) {
|
||||||
|
unset($requiredFields[self::SERIALIZE_VALUE__VERSION]);
|
||||||
|
} else {
|
||||||
|
$requiredFields[self::SERIALIZE_VALUE__VERSION] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the default data field (_0) is explicitly not wanted
|
||||||
|
if (($requiredFields[self::SERIALIZE_VALUE__DATA] ?? true) === null) {
|
||||||
|
unset($requiredFields[self::SERIALIZE_VALUE__DATA]);
|
||||||
|
} else {
|
||||||
|
$requiredFields[self::SERIALIZE_VALUE__DATA] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
|
||||||
|
foreach ($requiredFields as $field => $required) {
|
||||||
|
if (is_int($field)) {
|
||||||
|
$result[$required] = false;
|
||||||
|
} else {
|
||||||
|
$result[$field] = (bool)$required;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$requiredFields = $result;
|
||||||
|
|
||||||
|
$result = self::validatedObject();
|
||||||
|
|
||||||
|
foreach ($requiredFields as $field => $required) {
|
||||||
|
// allows us to check if the value was set
|
||||||
|
$value = $this;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* $serialized may be a \stdClass object created by `static::unserialize()`. So check for the required property
|
||||||
|
* based on the given data type
|
||||||
|
*
|
||||||
|
* @see static::unserialize()
|
||||||
|
*/
|
||||||
|
if ($isObject) {
|
||||||
|
if (method_exists($serialized, '__isset') ? $serialized->__isset($field) : property_exists($serialized, $field)) {
|
||||||
|
$value = &$serialized->{$field};
|
||||||
|
}
|
||||||
|
} elseif (is_array($serialized)) {
|
||||||
|
if (array_key_exists($field, $serialized)) {
|
||||||
|
$value = &$serialized[$field];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if a value has been retrieved
|
||||||
|
if ($value === $this) {
|
||||||
|
if (!$required) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$throw) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidArgumentValueException(
|
||||||
|
sprintf('Required field %s not found in serialized data for %s', $field, static::class)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->validateClassIncomplete($value, $found)) {
|
||||||
|
if (!$throw) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidArgumentTypeException(
|
||||||
|
sprintf('Invalid classes found in serialized data: %s', implode(', ', array_filter($found)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$result->$field = &$value;
|
||||||
|
|
||||||
|
// destroy reference
|
||||||
|
unset($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
$serialized = $result;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws InvalidConfigTypeException
|
||||||
|
*/
|
||||||
|
protected function validateClassInheritance($class, bool $throw, bool $strict = true): bool
|
||||||
|
{
|
||||||
|
if (!is_string($class)) {
|
||||||
|
if (!$throw) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidConfigTypeException(sprintf(
|
||||||
|
"Invalid class property type for %s: %s",
|
||||||
|
static::class,
|
||||||
|
get_debug_type($class)
|
||||||
|
), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!class_exists($class)) {
|
||||||
|
if (!$throw) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidConfigTypeException(sprintf(
|
||||||
|
"Invalid class name or non-existing class for %s: %s",
|
||||||
|
static::class,
|
||||||
|
get_debug_type($class)
|
||||||
|
), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
$parentClass = $strict ? static::class : self::class;
|
||||||
|
|
||||||
|
if ($class !== $parentClass && !is_subclass_of($class, $parentClass)) {
|
||||||
|
if (!$throw) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidConfigTypeException(sprintf(
|
||||||
|
"Class %s is not a subclass of %s",
|
||||||
|
get_debug_type($class),
|
||||||
|
$parentClass
|
||||||
|
), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param iterable|\stdClass|null $serialized
|
||||||
|
* @param array|null $found Output parameter returning
|
||||||
|
* - `null` if no incomplete class wos found, or
|
||||||
|
* - a list of class names as key and boolean value indicating that the class has been
|
||||||
|
* - invalid (true) or
|
||||||
|
* - valid (false).
|
||||||
|
* To get a list of all invalid classes, use `array_flip(array_filter((array)$found))`
|
||||||
|
* @param bool $throw
|
||||||
|
* @param int|null $recursion recursion will be performed for the level as indicated by this parameter.
|
||||||
|
* If `null` the default value of 1 will be applied
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
* @throws InvalidConfigTypeException
|
||||||
|
*/
|
||||||
|
protected function validateClassIncomplete(&$serialized, ?array &$found = null, bool $throw = true, ?int $recursion = 1): bool
|
||||||
|
{
|
||||||
|
$found = [];
|
||||||
|
$recursion ??= 1;
|
||||||
|
|
||||||
|
if ($serialized === null || is_scalar($serialized)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($serialized as $key => $item) {
|
||||||
|
if (is_scalar($item)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$changed = false;
|
||||||
|
|
||||||
|
if ($item instanceof __PHP_Incomplete_Class) {
|
||||||
|
// data of the incomplete class can only be accessed through iteration
|
||||||
|
$data = [];
|
||||||
|
$class = null;
|
||||||
|
|
||||||
|
foreach ($item as $property => $value) {
|
||||||
|
if ($property === '__PHP_Incomplete_Class_Name') {
|
||||||
|
// check if the class name inherits from StdClass
|
||||||
|
$validateClassInheritance = $this->validateClassInheritance($value, $throw, false);
|
||||||
|
|
||||||
|
// Save the result for return value.
|
||||||
|
// We store `true` for invalid, `false` for valid classes.
|
||||||
|
// This allows easy filtering invalid classes by using `array_filter($found)`
|
||||||
|
$found[$value] = !$validateClassInheritance;
|
||||||
|
|
||||||
|
if ($validateClassInheritance) {
|
||||||
|
// would be an alternative, but possibly requires more computational resources:
|
||||||
|
// $subitem = unserialize(serialize($subitem), ['allowed_classes' => [self::class, static::class, StdClass::class, $value]]);
|
||||||
|
|
||||||
|
// create a reflection class to later instantiate the new object
|
||||||
|
try {
|
||||||
|
$class = new ReflectionClass($value);
|
||||||
|
} catch (ReflectionException $e) {
|
||||||
|
$found[$value] = true;
|
||||||
|
|
||||||
|
Yii::warning(
|
||||||
|
sprintf(
|
||||||
|
"Reflection Exception occurred while validating metadata! %s",
|
||||||
|
serialize($item)
|
||||||
|
),
|
||||||
|
'File'
|
||||||
|
);
|
||||||
|
|
||||||
|
continue 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// loop through the other properties
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Yii::warning(
|
||||||
|
sprintf("Invalid metadata found and removed! %s", serialize($item)),
|
||||||
|
'File'
|
||||||
|
);
|
||||||
|
|
||||||
|
$item = null;
|
||||||
|
|
||||||
|
// skip the other properties and go to the next `$subitem`
|
||||||
|
continue 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// store the data in our array
|
||||||
|
$data[$property] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// now create an instance without calling the constructor ...
|
||||||
|
$item = $class->newInstanceWithoutConstructor();
|
||||||
|
|
||||||
|
// ... and initialize the object with the obtained data (as `unserialize` would do)
|
||||||
|
$item->__unserialize($data);
|
||||||
|
|
||||||
|
$changed = true;
|
||||||
|
$data = null;
|
||||||
|
$class = null;
|
||||||
|
} elseif ((is_iterable($item) || $item instanceof \stdClass) && $recursion) {
|
||||||
|
$incomplete = $this->validateClassIncomplete($item, $newFound, $throw, $recursion - 1);
|
||||||
|
|
||||||
|
$found = ArrayHelper::merge($found, (array)$newFound);
|
||||||
|
|
||||||
|
if (!$incomplete) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($changed) {
|
||||||
|
if (is_array($serialized) || $serialized instanceof ArrayAccess) {
|
||||||
|
$serialized[$key] = $item;
|
||||||
|
} else {
|
||||||
|
$serialized->$key = $item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($found === null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// since valid classes carry a `false` value, while invalid classes carry a `true` value,
|
||||||
|
// we can simply filter the array and only invalid classes will remain
|
||||||
|
return empty(array_filter($found));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function validatedObject(): \stdClass
|
||||||
|
{
|
||||||
|
if (self::$validatedObject === null) {
|
||||||
|
self::$validatedObject = new class () extends \stdClass {
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
$class = get_class(self::$validatedObject);
|
||||||
|
|
||||||
|
return new $class();
|
||||||
|
}
|
||||||
|
}
|
352
protected/humhub/libs/StdClassConfig.php
Normal file
352
protected/humhub/libs/StdClassConfig.php
Normal file
@ -0,0 +1,352 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @link https://www.humhub.org/
|
||||||
|
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
|
||||||
|
* @license https://www.humhub.com/licences
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace humhub\libs;
|
||||||
|
|
||||||
|
use Error;
|
||||||
|
use humhub\exceptions\InvalidConfigTypeException;
|
||||||
|
use ReflectionClass;
|
||||||
|
use ReflectionException;
|
||||||
|
use ReflectionObject;
|
||||||
|
use RuntimeException;
|
||||||
|
use SplObjectStorage;
|
||||||
|
use Throwable;
|
||||||
|
use UnexpectedValueException;
|
||||||
|
use WeakReference;
|
||||||
|
use yii\base\UnknownPropertyException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WARNING: This class and its API is still in experimental state. Expect changes in 1.16 (ToDo)
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* @codingStandardsIgnoreFile PSR2.Classes.PropertyDeclaration.Underscore
|
||||||
|
* @since 1.15 This class and its API is still in experimental state. Expect changes in 1.16 (ToDo)
|
||||||
|
* @internal (ToDo)
|
||||||
|
*/
|
||||||
|
class StdClassConfig extends StdClass
|
||||||
|
{
|
||||||
|
public const SERIALIZE_FORMAT = 1;
|
||||||
|
protected const SERIALIZE_VALUE__PARENT_FIXED = '_1';
|
||||||
|
protected const SERIALIZE_VALUE__CONFIG_FIXED = '_2';
|
||||||
|
protected const SERIALIZE_VALUE__CLASS = 'class';
|
||||||
|
|
||||||
|
protected const UNSERIALIZE_REQUIRED_VALUES = [
|
||||||
|
self::SERIALIZE_VALUE__CLASS,
|
||||||
|
self::SERIALIZE_VALUE__PARENT_FIXED,
|
||||||
|
self::SERIALIZE_VALUE__CONFIG_FIXED,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var mixed|null
|
||||||
|
*/
|
||||||
|
public $default;
|
||||||
|
|
||||||
|
// StdClassConfigurable meta-properties
|
||||||
|
|
||||||
|
protected bool $__StdClassConfigurable_isFixed = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var bool Denotes if the parent StdClass is loading, i.e., running the __construct() method
|
||||||
|
*/
|
||||||
|
protected bool $__StdClassConfigurable_loading = true;
|
||||||
|
|
||||||
|
// StdClassConfig meta-properties
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var bool Denotes if dynamic properties can be added (false) or the set of properties is fix.
|
||||||
|
*/
|
||||||
|
protected bool $__StdClassConfig_isFixed = false;
|
||||||
|
|
||||||
|
private static SplObjectStorage $__StdClassConfig_config;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var WeakReference Holding a reference to teh parent objet without increasing the reference count
|
||||||
|
* @see static::getParent()
|
||||||
|
* @see static::getReflection()
|
||||||
|
*/
|
||||||
|
private WeakReference $__StdClassConfig_parent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param StdClassConfigurable $parent
|
||||||
|
* @param mixed ...$initialValues
|
||||||
|
*
|
||||||
|
* @noinspection MagicMethodsValidityInspection
|
||||||
|
* @throws InvalidConfigTypeException
|
||||||
|
*/
|
||||||
|
public function __construct(StdClassConfigurable $parent, ...$initialValues)
|
||||||
|
{
|
||||||
|
$this->__StdClassConfig_parent = WeakReference::create($parent);
|
||||||
|
|
||||||
|
return parent::__construct(...$initialValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws UnknownPropertyException
|
||||||
|
*/
|
||||||
|
public function __get($name)
|
||||||
|
{
|
||||||
|
if ($this->__StdClassConfig_isFixed && !$this->__StdClassConfigurable_loading) {
|
||||||
|
throw new UnknownPropertyException('Getting unknown property: ' . static::class . '::$' . $name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::__get($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws UnknownPropertyException
|
||||||
|
*/
|
||||||
|
public function __set($name, $value)
|
||||||
|
{
|
||||||
|
if ($this->__StdClassConfig_isFixed && !$this->__StdClassConfigurable_loading) {
|
||||||
|
throw new UnknownPropertyException('Setting unknown property: ' . static::class . '::$' . $name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::__set($name, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __serialize(): array
|
||||||
|
{
|
||||||
|
$data = parent::__serialize();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ToDo: this can be removed in PHP 8
|
||||||
|
*
|
||||||
|
* @see StdClass::isFieldModified()
|
||||||
|
*/
|
||||||
|
if (PHP_MAJOR_VERSION < 8) {
|
||||||
|
if (($data[self::SERIALIZE_VALUE__DATA]['default'] ?? null) === null) {
|
||||||
|
unset($data[self::SERIALIZE_VALUE__DATA]['default']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($data[self::SERIALIZE_VALUE__DATA]) === 0) {
|
||||||
|
unset($data[self::SERIALIZE_VALUE__DATA]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->__StdClassConfigurable_isFixed) {
|
||||||
|
$data[self::SERIALIZE_VALUE__PARENT_FIXED] = $this->__StdClassConfigurable_isFixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->__StdClassConfig_isFixed) {
|
||||||
|
$data[self::SERIALIZE_VALUE__CONFIG_FIXED] = $this->__StdClassConfig_isFixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (static::class !== self::class) {
|
||||||
|
$data['class'] = static::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array|\stdClass $serialized
|
||||||
|
*
|
||||||
|
* @return StdClassConfig
|
||||||
|
* @noinspection MagicMethodsValidityInspection
|
||||||
|
* @throws InvalidConfigTypeException
|
||||||
|
*/
|
||||||
|
public function __unserialize($serialized)
|
||||||
|
{
|
||||||
|
$this->validateSerializedInput($serialized);
|
||||||
|
|
||||||
|
$class = $serialized->class ?? static::class;
|
||||||
|
unset($serialized->class);
|
||||||
|
|
||||||
|
// check if different class is required!
|
||||||
|
if ($class !== static::class) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
$class = new ReflectionClass($class);
|
||||||
|
$config = $class->newInstanceWithoutConstructor();
|
||||||
|
|
||||||
|
return $config->__unserialize($serialized);
|
||||||
|
} catch (ReflectionException $e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$parentFixed = $serialized->{self::SERIALIZE_VALUE__PARENT_FIXED} ?? false;
|
||||||
|
unset($serialized->{self::SERIALIZE_VALUE__PARENT_FIXED});
|
||||||
|
|
||||||
|
$configFixed = $serialized->{self::SERIALIZE_VALUE__CONFIG_FIXED} ?? false;
|
||||||
|
unset($serialized->{self::SERIALIZE_VALUE__CONFIG_FIXED});
|
||||||
|
|
||||||
|
$config = $this;
|
||||||
|
|
||||||
|
parent::__unserialize($serialized);
|
||||||
|
|
||||||
|
$this->__StdClassConfigurable_isFixed = $parentFixed;
|
||||||
|
$this->__StdClassConfig_isFixed = $configFixed;
|
||||||
|
|
||||||
|
return $config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function clear(): self
|
||||||
|
{
|
||||||
|
if ($this->isLoading()) {
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::clear();
|
||||||
|
|
||||||
|
$this->default = null;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isLoading(): bool
|
||||||
|
{
|
||||||
|
return $this->__StdClassConfigurable_loading;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setLoading(bool $loading): self
|
||||||
|
{
|
||||||
|
$this->__StdClassConfigurable_loading = $loading;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isFixed(): bool
|
||||||
|
{
|
||||||
|
return $this->__StdClassConfigurable_isFixed && !$this->__StdClassConfigurable_loading;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Once called, no (further) dynamic properties can be added
|
||||||
|
*
|
||||||
|
* @see static::$__StdClassConfig_isFixed
|
||||||
|
* @noinspection PhpUnused
|
||||||
|
*/
|
||||||
|
public function fixate(bool $fixed): self
|
||||||
|
{
|
||||||
|
$this->__StdClassConfigurable_isFixed = $this->__StdClassConfig_isFixed || $fixed;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool
|
||||||
|
* @noinspection PhpUnused
|
||||||
|
*/
|
||||||
|
public function isConfigFixed(): bool
|
||||||
|
{
|
||||||
|
return $this->__StdClassConfig_isFixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Once called, no (further) dynamic properties can be added
|
||||||
|
*
|
||||||
|
* @see static::$__StdClassConfig_isFixed
|
||||||
|
* @noinspection PhpUnused
|
||||||
|
*/
|
||||||
|
public function fixateConfig(bool $fixed): self
|
||||||
|
{
|
||||||
|
$this->__StdClassConfig_isFixed = $this->__StdClassConfig_isFixed || $fixed;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isModified(): bool
|
||||||
|
{
|
||||||
|
return $this->__StdClassConfigurable_isFixed
|
||||||
|
|| $this->__StdClassConfig_isFixed
|
||||||
|
|| parent::isModified();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function getReflection(): ReflectionObject
|
||||||
|
{
|
||||||
|
return new ReflectionObject($this->__StdClassConfig_parent->get() ?? new \stdClass());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws InvalidConfigTypeException
|
||||||
|
*/
|
||||||
|
public static function &getConfig(): self
|
||||||
|
{
|
||||||
|
$trace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 3);
|
||||||
|
|
||||||
|
$i = 1;
|
||||||
|
$destroy = $trace[$i]['class'] === static::class && $trace[$i]['function'] === 'destroyConfig';
|
||||||
|
|
||||||
|
if ($destroy) {
|
||||||
|
++$i;
|
||||||
|
}
|
||||||
|
|
||||||
|
$parent = $trace[$i]['object'] ?? null;
|
||||||
|
|
||||||
|
if (!$parent instanceof StdClassConfigurable) {
|
||||||
|
throw new RuntimeException(sprintf('Method %s can only be called from a %s instance itself', __METHOD__,
|
||||||
|
StdClassConfigurable::class));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$config = self::$__StdClassConfig_config[$parent];
|
||||||
|
} catch (UnexpectedValueException $e) {
|
||||||
|
$config = null;
|
||||||
|
} catch (Error $e) {
|
||||||
|
if ($e->getMessage() === 'Typed static property ' . self::class . '::$__StdClassConfig_config must not be accessed before initialization') {
|
||||||
|
self::$__StdClassConfig_config = new SplObjectStorage();
|
||||||
|
$config = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($destroy) {
|
||||||
|
if ($config) {
|
||||||
|
unset(self::$__StdClassConfig_config[$parent]);
|
||||||
|
}
|
||||||
|
} elseif ($config === null) {
|
||||||
|
self::$__StdClassConfig_config[$parent] = $config = new static($parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Throwable
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public static function destroyConfig(): void
|
||||||
|
{
|
||||||
|
static::getConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function getParent(): ?StdClassConfigurable
|
||||||
|
{
|
||||||
|
return $this->__StdClassConfig_parent->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws InvalidConfigTypeException
|
||||||
|
*/
|
||||||
|
protected function validateSerializedInput(&$serialized, ?array $requiredFields = self::UNSERIALIZE_REQUIRED_VALUES, bool $throw = true): ?bool
|
||||||
|
{
|
||||||
|
// this is used to identify already-validated data
|
||||||
|
self::$validatedObject ??= self::validatedObject();
|
||||||
|
|
||||||
|
if ($serialized instanceof self::$validatedObject) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$valid = parent::validateSerializedInput($serialized, $requiredFields, $throw);
|
||||||
|
|
||||||
|
if ($valid === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$class = $serialized->{self::SERIALIZE_VALUE__CLASS} ?? null;
|
||||||
|
|
||||||
|
if ($class === null || $this->validateClassInheritance($class, $throw)) {
|
||||||
|
return $valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
296
protected/humhub/libs/StdClassConfigurable.php
Normal file
296
protected/humhub/libs/StdClassConfigurable.php
Normal file
@ -0,0 +1,296 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @link https://www.humhub.org/
|
||||||
|
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
|
||||||
|
* @license https://www.humhub.com/licences
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace humhub\libs;
|
||||||
|
|
||||||
|
use humhub\exceptions\InvalidArgumentTypeException;
|
||||||
|
use humhub\exceptions\InvalidConfigTypeException;
|
||||||
|
use Throwable;
|
||||||
|
use yii\base\UnknownPropertyException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WARNING: This class and its API is still in experimental state. Expect changes in 1.16 (ToDo)
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* Extending from \humhub\libs\StdClass, this class some additional features:
|
||||||
|
* - a defaultValue can be set which will be used in case a non-existent property is read: \
|
||||||
|
* `static::setDefaultValue($value)`
|
||||||
|
* `static::getDefaultValue()`
|
||||||
|
* - the object provides a static::config() accessor to a separate property namespace
|
||||||
|
* - allows the object to be fixated, so that now additional property can be added \
|
||||||
|
* with `static::fixate()`
|
||||||
|
* (this is not write-protection, just disallowing new properties!)
|
||||||
|
*
|
||||||
|
* @since 1.15 This class and its API is still in experimental state. Expect changes in 1.16 (ToDo)
|
||||||
|
* @internal (ToDo)
|
||||||
|
* @see StdClassConfig
|
||||||
|
*/
|
||||||
|
class StdClassConfigurable extends StdClass
|
||||||
|
{
|
||||||
|
public const SERIALIZE_FORMAT = 1;
|
||||||
|
protected const SERIALIZE_VALUE__CLASS = 'class';
|
||||||
|
protected const SERIALIZE_VALUE__CONFIG = 'config';
|
||||||
|
|
||||||
|
protected const UNSERIALIZE_REQUIRED_VALUES = [
|
||||||
|
self::SERIALIZE_VALUE__DATA,
|
||||||
|
self::SERIALIZE_VALUE__CLASS,
|
||||||
|
self::SERIALIZE_VALUE__CONFIG,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inerhitdoc
|
||||||
|
* @noinspection MagicMethodsValidityInspection
|
||||||
|
*/
|
||||||
|
public function __construct(...$args)
|
||||||
|
{
|
||||||
|
$config = $this->config();
|
||||||
|
|
||||||
|
parent::__construct(...$args);
|
||||||
|
|
||||||
|
$config->setLoading(false);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __destruct()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
StdClassConfig::destroyConfig();
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws UnknownPropertyException
|
||||||
|
*/
|
||||||
|
public function __get($name)
|
||||||
|
{
|
||||||
|
if ($name === null) {
|
||||||
|
return $this->getDefaultValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->config()->isFixed()) {
|
||||||
|
throw new UnknownPropertyException('Getting unknown property: ' . static::class . '::$' . $name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::__get($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws UnknownPropertyException
|
||||||
|
*/
|
||||||
|
public function __set($name, $value)
|
||||||
|
{
|
||||||
|
if ($name === null) {
|
||||||
|
return $this->setDefaultValue($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->config()->isFixed()) {
|
||||||
|
throw new UnknownPropertyException('Setting unknown property: ' . static::class . '::$' . $name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::__set($name, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __isset($name)
|
||||||
|
{
|
||||||
|
if ($name === null) {
|
||||||
|
return $this->hasDefaultValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::__isset($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __unset($name)
|
||||||
|
{
|
||||||
|
if ($name === null) {
|
||||||
|
$this->setDefaultValue(null);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::__unset($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __serialize(): array
|
||||||
|
{
|
||||||
|
$isSerializing = self::isSerializing($this);
|
||||||
|
|
||||||
|
$data = parent::__serialize();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loop through the data and remove any unmodified instance of `StdClass`.
|
||||||
|
*
|
||||||
|
* (Use $item as a reference so that arrays don't get copied.)
|
||||||
|
*
|
||||||
|
* @noinspection PhpParameterByRefIsNotUsedAsReferenceInspection
|
||||||
|
*/
|
||||||
|
foreach ($data[self::SERIALIZE_VALUE__DATA] ?? [] as $key => &$item) {
|
||||||
|
if ($item instanceof StdClass && !$item->isModified()) {
|
||||||
|
unset($data[self::SERIALIZE_VALUE__DATA][$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unset($item);
|
||||||
|
|
||||||
|
if ($isSerializing && static::class !== self::class) {
|
||||||
|
$data[self::SERIALIZE_VALUE__CLASS] = static::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data[self::SERIALIZE_VALUE__CONFIG] = (object)$this->config()->__serialize();
|
||||||
|
|
||||||
|
self::isSerializing($this, !$isSerializing);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array|\stdClass $serialized
|
||||||
|
*
|
||||||
|
* @return self
|
||||||
|
* @throws InvalidArgumentTypeException|InvalidConfigTypeException
|
||||||
|
* @noinspection MagicMethodsValidityInspection
|
||||||
|
*/
|
||||||
|
public function __unserialize($serialized)
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* $serialized may be a \stdClass object created by `static::unserialize()`
|
||||||
|
*
|
||||||
|
* @see static::unserialize()
|
||||||
|
*/
|
||||||
|
$this->validateSerializedInput($serialized);
|
||||||
|
|
||||||
|
// clear only after validation was successful
|
||||||
|
$this->clear();
|
||||||
|
|
||||||
|
$config = &$serialized->{self::SERIALIZE_VALUE__CONFIG};
|
||||||
|
unset($serialized->{self::SERIALIZE_VALUE__CONFIG});
|
||||||
|
|
||||||
|
$return = parent::__unserialize($serialized);
|
||||||
|
|
||||||
|
$return->config()
|
||||||
|
->__unserialize($config)
|
||||||
|
->setLoading(false);
|
||||||
|
|
||||||
|
return $return;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function &config(): StdClassConfig
|
||||||
|
{
|
||||||
|
return StdClassConfig::getConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function clear(): self
|
||||||
|
{
|
||||||
|
$config = $this->config();
|
||||||
|
|
||||||
|
if ($config->isLoading()) {
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
$config->clear();
|
||||||
|
|
||||||
|
return parent::clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isModified(): bool
|
||||||
|
{
|
||||||
|
return parent::isModified() || $this->config()->isModified();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isFixed(): bool
|
||||||
|
{
|
||||||
|
return $this->config()->isFixed();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Once called, no (further) dynamic properties can be added
|
||||||
|
*
|
||||||
|
* @see StdClassConfig::fixate()
|
||||||
|
* @noinspection PhpUnused
|
||||||
|
*/
|
||||||
|
public function fixate(): self
|
||||||
|
{
|
||||||
|
$this->config()->fixate(true);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return mixed|null
|
||||||
|
*/
|
||||||
|
public function getDefaultValue()
|
||||||
|
{
|
||||||
|
return $this->config()->default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $value
|
||||||
|
*
|
||||||
|
* @return static
|
||||||
|
*/
|
||||||
|
public function setDefaultValue($value): self
|
||||||
|
{
|
||||||
|
$this->config()->default = $value;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasDefaultValue(): bool
|
||||||
|
{
|
||||||
|
return $this->config()->default !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function is used internally to validate property names
|
||||||
|
*
|
||||||
|
* @param $name
|
||||||
|
* @param string $method
|
||||||
|
* @param string|array $parameter
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function validatePropertyName($name, string $method, string $parameter = '$name'): ?string
|
||||||
|
{
|
||||||
|
if ($name === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::validatePropertyName($name, $method, $parameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws InvalidConfigTypeException
|
||||||
|
*/
|
||||||
|
protected function validateSerializedInput(&$serialized, ?array $requiredFields = self::UNSERIALIZE_REQUIRED_VALUES, bool $throw = true): ?bool
|
||||||
|
{
|
||||||
|
// this is used to identify already-validated data
|
||||||
|
self::$validatedObject ??= self::validatedObject();
|
||||||
|
|
||||||
|
if ($serialized instanceof self::$validatedObject) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$valid = parent::validateSerializedInput($serialized, $requiredFields, $throw);
|
||||||
|
|
||||||
|
if ($valid === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$class = $serialized->{self::SERIALIZE_VALUE__CLASS} ?? null;
|
||||||
|
|
||||||
|
if ($class !== null && !$this->validateClassInheritance($class, $throw)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$config = &$serialized->{self::SERIALIZE_VALUE__CONFIG};
|
||||||
|
|
||||||
|
$this->validateClassIncomplete($config);
|
||||||
|
|
||||||
|
return $valid;
|
||||||
|
}
|
||||||
|
}
|
24
protected/humhub/modules/file/libs/Metadata.php
Normal file
24
protected/humhub/modules/file/libs/Metadata.php
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @link https://www.humhub.org/
|
||||||
|
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
|
||||||
|
* @license https://www.humhub.com/licences
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace humhub\modules\file\libs;
|
||||||
|
|
||||||
|
use humhub\libs\StdClassConfigurable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WARNING: This class and its API is still in experimental state. Expect changes in 1.16 (ToDo)
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* This class is used to access the data in the `File::$metadata` field.
|
||||||
|
*
|
||||||
|
* @since 1.15; this class and its API is still in experimental state. Expect changes in 1.16 (ToDo)
|
||||||
|
* @internal (ToDo)
|
||||||
|
*/
|
||||||
|
class Metadata extends StdClassConfigurable
|
||||||
|
{
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @link https://www.humhub.org/
|
||||||
|
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
|
||||||
|
* @license https://www.humhub.com/licences
|
||||||
|
*/
|
||||||
|
|
||||||
|
use humhub\components\Migration;
|
||||||
|
use humhub\modules\file\models\File;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add and film GUID column
|
||||||
|
*/
|
||||||
|
class m230618_135510_file_add_metadata_column extends Migration
|
||||||
|
{
|
||||||
|
// protected properties
|
||||||
|
protected string $table;
|
||||||
|
|
||||||
|
public function __construct($config = [])
|
||||||
|
{
|
||||||
|
$this->table = File::tableName();
|
||||||
|
parent::__construct($config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function safeUp(): void
|
||||||
|
{
|
||||||
|
|
||||||
|
$this->safeAddColumn(
|
||||||
|
$this->table,
|
||||||
|
'metadata',
|
||||||
|
$this->string(4000)
|
||||||
|
->after('size')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -11,10 +11,12 @@ namespace humhub\modules\file\models;
|
|||||||
use humhub\components\ActiveRecord;
|
use humhub\components\ActiveRecord;
|
||||||
use humhub\components\behaviors\GUID;
|
use humhub\components\behaviors\GUID;
|
||||||
use humhub\components\behaviors\PolymorphicRelation;
|
use humhub\components\behaviors\PolymorphicRelation;
|
||||||
|
use humhub\libs\StdClass;
|
||||||
use humhub\modules\content\components\ContentActiveRecord;
|
use humhub\modules\content\components\ContentActiveRecord;
|
||||||
use humhub\modules\content\components\ContentAddonActiveRecord;
|
use humhub\modules\content\components\ContentAddonActiveRecord;
|
||||||
use humhub\modules\file\components\StorageManager;
|
use humhub\modules\file\components\StorageManager;
|
||||||
use humhub\modules\file\components\StorageManagerInterface;
|
use humhub\modules\file\components\StorageManagerInterface;
|
||||||
|
use humhub\modules\file\libs\Metadata;
|
||||||
use humhub\modules\user\models\User;
|
use humhub\modules\user\models\User;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
use Yii;
|
use Yii;
|
||||||
@ -40,6 +42,26 @@ use yii\web\UploadedFile;
|
|||||||
* @property string $title
|
* @property string $title
|
||||||
* @property string $mime_type
|
* @property string $mime_type
|
||||||
* @property string $size
|
* @property string $size
|
||||||
|
* @property-read Metadata $metadata since 1.15. Note, $metadata is still experimental. Expect changes in v1.16 (ToDo).
|
||||||
|
* This property is read-only in the sense that no new instance be assigned to the model.
|
||||||
|
* Edit data always by working on the object itself.
|
||||||
|
* You best retrieve is using `static::getMetadata()`.
|
||||||
|
* E.g, to set a value you could do:
|
||||||
|
* ```
|
||||||
|
* // setting a single value
|
||||||
|
* $model->getMetadata()->property1 = "some value";
|
||||||
|
* // or
|
||||||
|
* $model->getMetadata()['property2'] = "some other value";
|
||||||
|
*
|
||||||
|
* // setting multiple values
|
||||||
|
* $metadata = $model->getMetadata();
|
||||||
|
* $metadata->property1 = "some value";
|
||||||
|
* $metadata['property2'] = "some other value";
|
||||||
|
*
|
||||||
|
* // alternatively, the `Metadata::addValues()` method can be used:
|
||||||
|
* $model->getMetadata()->addValues(['property1' => "some value", 'property2' => "some other value"] = "some other value";
|
||||||
|
* ```
|
||||||
|
*
|
||||||
* @property string|null $object_model
|
* @property string|null $object_model
|
||||||
* @property integer|null $object_id
|
* @property integer|null $object_id
|
||||||
* @property integer|null $content_id
|
* @property integer|null $content_id
|
||||||
@ -152,6 +174,15 @@ class File extends FileCompat
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function __get($name)
|
||||||
|
{
|
||||||
|
if ($name === 'metadata') {
|
||||||
|
return $this->getMetadata();
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::__get($name);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a query for [[FileHistory]].
|
* Gets a query for [[FileHistory]].
|
||||||
*
|
*
|
||||||
@ -180,6 +211,12 @@ class File extends FileCompat
|
|||||||
|
|
||||||
$this->sort_order ??= 0;
|
$this->sort_order ??= 0;
|
||||||
|
|
||||||
|
$metadata = $this->getAttribute('metadata');
|
||||||
|
|
||||||
|
if (($metadata instanceof StdClass) && !$metadata->isModified()) {
|
||||||
|
$this->setAttribute('metadata', null);
|
||||||
|
}
|
||||||
|
|
||||||
return parent::beforeSave($insert);
|
return parent::beforeSave($insert);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -316,6 +353,40 @@ class File extends FileCompat
|
|||||||
return $this->object_model === get_class($record) && $this->object_id == $record->getPrimaryKey();
|
return $this->object_model === get_class($record) && $this->object_id == $record->getPrimaryKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Metadata
|
||||||
|
*/
|
||||||
|
public function getMetadata(): Metadata
|
||||||
|
{
|
||||||
|
/** @var Metadata|null $metadata */
|
||||||
|
$metadata = $this->getAttribute('metadata');
|
||||||
|
|
||||||
|
if ($metadata instanceof Metadata) {
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
$metadata = new Metadata($metadata);
|
||||||
|
|
||||||
|
$this->setAttribute('metadata', $metadata);
|
||||||
|
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string|array $metadata
|
||||||
|
*
|
||||||
|
* @return File
|
||||||
|
*/
|
||||||
|
public function setMetadata($metadata): File
|
||||||
|
{
|
||||||
|
/** @var Metadata|null $md */
|
||||||
|
$md = $this->metadata;
|
||||||
|
|
||||||
|
$md->addValues($metadata);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the StorageManager
|
* Returns the StorageManager
|
||||||
*
|
*
|
||||||
@ -379,15 +450,15 @@ class File extends FileCompat
|
|||||||
$store = $this->getStore();
|
$store = $this->getStore();
|
||||||
|
|
||||||
if ($file instanceof UploadedFile) {
|
if ($file instanceof UploadedFile) {
|
||||||
$this->getStore()->set($file);
|
$store->set($file);
|
||||||
} elseif ($file instanceof File) {
|
} elseif ($file instanceof self) {
|
||||||
if ($file->isAssigned()) {
|
if ($file->isAssigned()) {
|
||||||
throw new InvalidArgumentException('Already assigned File records cannot stored as another File record.');
|
throw new InvalidArgumentException('Already assigned File records cannot stored as another File record.');
|
||||||
}
|
}
|
||||||
$this->getStore()->setByPath($file->getStore()->get());
|
$store->setByPath($file->getStore()->get());
|
||||||
$file->delete();
|
$file->delete();
|
||||||
} elseif (is_string($file) && is_file($file)) {
|
} elseif (is_string($file) && is_file($file)) {
|
||||||
$this->getStore()->setByPath($file);
|
$store->setByPath($file);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->afterNewStoredFile();
|
$this->afterNewStoredFile();
|
||||||
|
@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @link https://www.humhub.org/
|
||||||
|
* @copyright Copyright (c) 2019-2023 HumHub GmbH & Co. KG
|
||||||
|
* @license https://www.humhub.com/licences
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace tests\codeception\unit\modules\file;
|
||||||
|
|
||||||
|
use humhub\modules\file\libs\Metadata;
|
||||||
|
use tests\codeception\_support\HumHubDbTestCase;
|
||||||
|
|
||||||
|
class MetadataTest extends HumHubDbTestCase
|
||||||
|
{
|
||||||
|
private static $useData = false;
|
||||||
|
|
||||||
|
public function _fixtures(): array
|
||||||
|
{
|
||||||
|
return self::$useData
|
||||||
|
? parent::_fixtures()
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testInstantiationStdClass()
|
||||||
|
{
|
||||||
|
$serialized = 'O:33:"humhub\modules\file\libs\Metadata":2:{s:1:"v";i:1;s:6:"config";O:8:"stdClass":1:{s:1:"v";i:1;}}';
|
||||||
|
|
||||||
|
$instance = new Metadata();
|
||||||
|
|
||||||
|
static::assertInstanceOf(Metadata::class, $instance);
|
||||||
|
static::assertCount(0, $instance);
|
||||||
|
static::assertEquals([], $instance->fields());
|
||||||
|
static::assertEquals($serialized, $instance->serialize());
|
||||||
|
|
||||||
|
$serialized = 'O:33:"humhub\modules\file\libs\Metadata":3:{s:1:"v";i:1;s:2:"_0";a:1:{s:3:"foo";s:3:"bar";}s:6:"config";O:8:"stdClass":1:{s:1:"v";i:1;}}';
|
||||||
|
|
||||||
|
$instance = new Metadata(['foo' => 'bar']);
|
||||||
|
static::assertInstanceOf(Metadata::class, $instance);
|
||||||
|
static::assertCount(1, $instance);
|
||||||
|
static::assertEquals(['foo' => 'foo'], $instance->fields());
|
||||||
|
static::assertEquals(['foo' => 'bar'], $instance->toArray());
|
||||||
|
static::assertEquals($serialized, $instance->serialize());
|
||||||
|
|
||||||
|
$instance = new Metadata($serialized);
|
||||||
|
static::assertInstanceOf(Metadata::class, $instance);
|
||||||
|
static::assertCount(1, $instance);
|
||||||
|
static::assertEquals(['foo' => 'foo'], $instance->fields());
|
||||||
|
static::assertEquals(['foo' => 'bar'], $instance->toArray());
|
||||||
|
static::assertEquals($serialized, $instance->serialize());
|
||||||
|
}
|
||||||
|
}
|
164
protected/humhub/tests/codeception/unit/libs/StdClassTest.php
Normal file
164
protected/humhub/tests/codeception/unit/libs/StdClassTest.php
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @link https://www.humhub.org/
|
||||||
|
* @copyright Copyright (c) 2018-2023 HumHub GmbH & Co. KG
|
||||||
|
* @license https://www.humhub.com/licences
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace humhub\tests\codeception\unit;
|
||||||
|
|
||||||
|
use Codeception\Test\Unit;
|
||||||
|
use humhub\exceptions\InvalidArgumentTypeException;
|
||||||
|
use humhub\exceptions\InvalidArgumentValueException;
|
||||||
|
use humhub\libs\StdClass;
|
||||||
|
use humhub\libs\StdClassConfigurable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class MimeHelperTest
|
||||||
|
*/
|
||||||
|
class StdClassTest extends Unit
|
||||||
|
{
|
||||||
|
public function testInstantiationStdClass()
|
||||||
|
{
|
||||||
|
$serialized = 'O:20:"humhub\libs\StdClass":1:{s:1:"v";i:1;}';
|
||||||
|
|
||||||
|
$instance = new StdClass();
|
||||||
|
|
||||||
|
static::assertInstanceOf(StdClass::class, $instance);
|
||||||
|
static::assertCount(0, $instance);
|
||||||
|
static::assertEquals([], $instance->fields());
|
||||||
|
static::assertEquals($serialized, $instance->serialize());
|
||||||
|
static::assertEquals($serialized, serialize($instance));
|
||||||
|
|
||||||
|
$instance = new StdClass([]);
|
||||||
|
static::assertInstanceOf(StdClass::class, $instance);
|
||||||
|
static::assertCount(0, $instance);
|
||||||
|
static::assertEquals([], $instance->fields());
|
||||||
|
static::assertEquals($serialized, $instance->serialize());
|
||||||
|
|
||||||
|
$instance = new StdClass(null);
|
||||||
|
static::assertInstanceOf(StdClass::class, $instance);
|
||||||
|
static::assertCount(0, $instance);
|
||||||
|
|
||||||
|
$instance = new StdClass($serialized);
|
||||||
|
static::assertCount(0, $instance);
|
||||||
|
static::assertEquals([], $instance->fields());
|
||||||
|
static::assertEquals($serialized, $instance->serialize());
|
||||||
|
|
||||||
|
$instance = new StdClass('O:20:"humhub\libs\StdClass":2:{s:1:"v";i:1;i:0;a:0:{}}');
|
||||||
|
static::assertCount(0, $instance);
|
||||||
|
static::assertEquals([], $instance->fields());
|
||||||
|
static::assertEquals($serialized, $instance->serialize());
|
||||||
|
|
||||||
|
$serialized = 'O:20:"humhub\libs\StdClass":2:{s:1:"v";i:1;s:2:"_0";a:1:{s:3:"foo";s:3:"bar";}}';
|
||||||
|
|
||||||
|
$instance = new StdClass(['foo' => 'bar']);
|
||||||
|
static::assertInstanceOf(StdClass::class, $instance);
|
||||||
|
static::assertCount(1, $instance);
|
||||||
|
static::assertEquals(['foo' => 'foo'], $instance->fields());
|
||||||
|
static::assertEquals(['foo' => 'bar'], $instance->toArray());
|
||||||
|
static::assertEquals($serialized, $instance->serialize());
|
||||||
|
|
||||||
|
$instance = new StdClass($serialized);
|
||||||
|
static::assertCount(1, $instance);
|
||||||
|
static::assertEquals(['foo' => 'foo'], $instance->fields());
|
||||||
|
static::assertEquals(['foo' => 'bar'], $instance->toArray());
|
||||||
|
static::assertEquals($serialized, $instance->serialize());
|
||||||
|
|
||||||
|
$this->expectException(InvalidArgumentValueException::class);
|
||||||
|
$this->expectExceptionMessage('Argument $serialized passed to humhub\libs\StdClass::unserialize must be string starting with \'O:20:"humhub\libs\StdClass"\' - O:20:"some\dangerous\class":1:{s:4:"data";a:1:{s:3:"foo";s:3:"bar";}} given.');
|
||||||
|
|
||||||
|
new StdClass('O:20:"some\\dangerous\\class":1:{s:4:"data";a:1:{s:3:"foo";s:3:"bar";}}');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testInvalidInstantiationStdClassWithTrue()
|
||||||
|
{
|
||||||
|
$this->expectException(InvalidArgumentTypeException::class);
|
||||||
|
$this->expectExceptionMessage('Argument ...$args passed to humhub\libs\StdClass::addValues must be one of the following types: array, Traversable - bool given.');
|
||||||
|
|
||||||
|
new StdClass(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testInvalidInstantiationStdClassWithFalse()
|
||||||
|
{
|
||||||
|
$this->expectException(InvalidArgumentTypeException::class);
|
||||||
|
$this->expectExceptionMessage('Argument ...$args passed to humhub\libs\StdClass::addValues must be one of the following types: array, Traversable - bool given.');
|
||||||
|
|
||||||
|
new StdClass(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testInstantiationStdClassConfigurable()
|
||||||
|
{
|
||||||
|
$serialized = 'O:32:"humhub\libs\StdClassConfigurable":2:{s:1:"v";i:1;s:6:"config";O:8:"stdClass":1:{s:1:"v";i:1;}}';
|
||||||
|
|
||||||
|
$instance = new StdClassConfigurable();
|
||||||
|
static::assertInstanceOf(StdClassConfigurable::class, $instance);
|
||||||
|
static::assertCount(0, $instance);
|
||||||
|
static::assertEquals([], $instance->fields());
|
||||||
|
static::assertEquals($serialized, $instance->serialize());
|
||||||
|
|
||||||
|
// try again with the now initialized config storage
|
||||||
|
$instance = new StdClassConfigurable();
|
||||||
|
static::assertInstanceOf(StdClassConfigurable::class, $instance);
|
||||||
|
static::assertCount(0, $instance);
|
||||||
|
|
||||||
|
// try with different parameter values
|
||||||
|
|
||||||
|
$instance = new StdClassConfigurable([]);
|
||||||
|
static::assertInstanceOf(StdClassConfigurable::class, $instance);
|
||||||
|
static::assertCount(0, $instance);
|
||||||
|
static::assertEquals([], $instance->fields());
|
||||||
|
|
||||||
|
$instance = new StdClassConfigurable(null);
|
||||||
|
static::assertInstanceOf(StdClassConfigurable::class, $instance);
|
||||||
|
static::assertCount(0, $instance);
|
||||||
|
static::assertEquals([], $instance->fields());
|
||||||
|
|
||||||
|
$instance = new StdClassConfigurable($serialized);
|
||||||
|
static::assertInstanceOf(StdClassConfigurable::class, $instance);
|
||||||
|
static::assertCount(0, $instance);
|
||||||
|
static::assertEquals([], $instance->fields());
|
||||||
|
static::assertEquals($serialized, $instance->serialize());
|
||||||
|
|
||||||
|
$instance = new StdClassConfigurable('O:32:"humhub\libs\StdClassConfigurable":3:{s:1:"v";i:1;i:0;a:0:{}s:6:"config";O:8:"stdClass":5:{s:1:"v";i:1;s:2:"_0";a:1:{s:7:"default";N;}s:2:"_1";b:0;s:2:"_2";b:0;s:2:"_3";b:0;}}');
|
||||||
|
static::assertInstanceOf(StdClassConfigurable::class, $instance);
|
||||||
|
static::assertCount(0, $instance);
|
||||||
|
static::assertEquals([], $instance->fields());
|
||||||
|
static::assertEquals($serialized, $instance->serialize());
|
||||||
|
|
||||||
|
$serialized = 'O:32:"humhub\libs\StdClassConfigurable":3:{s:1:"v";i:1;s:2:"_0";a:1:{s:3:"foo";s:3:"bar";}s:6:"config";O:8:"stdClass":1:{s:1:"v";i:1;}}';
|
||||||
|
|
||||||
|
$instance = new StdClassConfigurable(['foo' => 'bar']);
|
||||||
|
static::assertInstanceOf(StdClassConfigurable::class, $instance);
|
||||||
|
static::assertCount(1, $instance);
|
||||||
|
static::assertEquals(['foo' => 'foo'], $instance->fields());
|
||||||
|
static::assertEquals(['foo' => 'bar'], $instance->toArray());
|
||||||
|
static::assertEquals($serialized, $instance->serialize());
|
||||||
|
|
||||||
|
$instance = new StdClassConfigurable($serialized);
|
||||||
|
static::assertInstanceOf(StdClassConfigurable::class, $instance);
|
||||||
|
static::assertCount(1, $instance);
|
||||||
|
static::assertEquals(['foo' => 'foo'], $instance->fields());
|
||||||
|
static::assertEquals(['foo' => 'bar'], $instance->toArray());
|
||||||
|
static::assertFalse($instance->isFixed());
|
||||||
|
static::assertEquals($serialized, $instance->serialize());
|
||||||
|
|
||||||
|
$serialized = 'O:32:"humhub\libs\StdClassConfigurable":3:{s:1:"v";i:1;s:2:"_0";a:1:{s:3:"foo";s:3:"bar";}s:6:"config";O:8:"stdClass":2:{s:1:"v";i:1;s:2:"_1";b:1;}}';
|
||||||
|
|
||||||
|
$instance = StdClassConfigurable::create(['foo' => 'bar'])->fixate();
|
||||||
|
static::assertInstanceOf(StdClassConfigurable::class, $instance);
|
||||||
|
static::assertCount(1, $instance);
|
||||||
|
static::assertEquals(['foo' => 'foo'], $instance->fields());
|
||||||
|
static::assertEquals(['foo' => 'bar'], $instance->toArray());
|
||||||
|
static::assertTrue($instance->isFixed(), 'StdClassConfigurable object is not fixated!');
|
||||||
|
static::assertEquals($serialized, $instance->serialize());
|
||||||
|
|
||||||
|
$instance = new StdClassConfigurable($serialized);
|
||||||
|
static::assertInstanceOf(StdClassConfigurable::class, $instance);
|
||||||
|
static::assertCount(1, $instance);
|
||||||
|
static::assertEquals(['foo' => 'foo'], $instance->fields());
|
||||||
|
static::assertTrue($instance->isFixed(), 'StdClassConfigurable object is not fixated!');
|
||||||
|
static::assertEquals($serialized, $instance->serialize());
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user